From f7e35bc86cd6fd06c4e47a788faa9800d0c741dd Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Wed, 18 Sep 2024 08:51:57 -0500 Subject: [PATCH] EVM: NttManagerWithoutRateLimiting --- evm/src/NttManager/NttManager.sol | 383 +-------- .../NttManagerWithoutRateLimiting.sol | 509 ++++++++++++ evm/src/interfaces/INttManager.sol | 3 + evm/test/IntegrationWithoutRateLimiting.t.sol | 743 ++++++++++++++++++ evm/test/mocks/MockNttManager.sol | 23 + 5 files changed, 1317 insertions(+), 344 deletions(-) create mode 100644 evm/src/NttManager/NttManagerWithoutRateLimiting.sol create mode 100755 evm/test/IntegrationWithoutRateLimiting.t.sol diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index b41c5f923..a50ff697d 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -14,11 +14,12 @@ import "../interfaces/INttToken.sol"; import "../interfaces/ITransceiver.sol"; import {ManagerBase} from "./ManagerBase.sol"; +import {NttManagerWithoutRateLimiting} from "./NttManagerWithoutRateLimiting.sol"; /// @title NttManager /// @author Wormhole Project Contributors. /// @notice The NttManager contract is responsible for managing the token -/// and associated transceivers. +/// and associated transceivers. It uses the rate limiter. /// /// @dev Each NttManager contract is associated with a single token but /// can be responsible for multiple transceivers. @@ -35,14 +36,12 @@ import {ManagerBase} from "./ManagerBase.sol"; /// to be too high, users will be refunded the difference. /// - (optional) a flag to indicate whether the transfer should be queued /// if the rate limit is exceeded -contract NttManager is INttManager, RateLimiter, ManagerBase { +contract NttManager is NttManagerWithoutRateLimiting, RateLimiter { using BytesParsing for bytes; using SafeERC20 for IERC20; using TrimmedAmountLib for uint256; using TrimmedAmountLib for TrimmedAmount; - string public constant NTT_MANAGER_VERSION = "1.1.0"; - // =============== Setup ================================================================= constructor( @@ -51,53 +50,16 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { uint16 _chainId, uint64 _rateLimitDuration, bool _skipRateLimiting - ) RateLimiter(_rateLimitDuration, _skipRateLimiting) ManagerBase(_token, _mode, _chainId) {} + ) + RateLimiter(_rateLimitDuration, _skipRateLimiting) + NttManagerWithoutRateLimiting(_token, _mode, _chainId) + {} - function __NttManager_init() internal onlyInitializing { - // check if the owner is the deployer of this contract - if (msg.sender != deployer) { - revert UnexpectedDeployer(deployer, msg.sender); - } - if (msg.value != 0) { - revert UnexpectedMsgValue(); - } - __PausedOwnable_init(msg.sender, msg.sender); - __ReentrancyGuard_init(); + function __NttManager_init() internal override onlyInitializing { + super.__NttManager_init(); _setOutboundLimit(TrimmedAmountLib.max(tokenDecimals())); } - function _initialize() internal virtual override { - __NttManager_init(); - _checkThresholdInvariants(); - _checkTransceiversInvariants(); - } - - // =============== Storage ============================================================== - - bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("ntt.peers")) - 1); - - // =============== Storage Getters/Setters ============================================== - - function _getPeersStorage() - internal - pure - returns (mapping(uint16 => NttManagerPeer) storage $) - { - uint256 slot = uint256(PEERS_SLOT); - assembly ("memory-safe") { - $.slot := slot - } - } - - // =============== Public Getters ======================================================== - - /// @inheritdoc INttManager - function getPeer( - uint16 chainId_ - ) external view returns (NttManagerPeer memory) { - return _getPeersStorage()[chainId_]; - } - // =============== Admin ============================================================== /// @inheritdoc INttManager @@ -106,43 +68,23 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { bytes32 peerContract, uint8 decimals, uint256 inboundLimit - ) public onlyOwner { - if (peerChainId == 0) { - revert InvalidPeerChainIdZero(); - } - if (peerContract == bytes32(0)) { - revert InvalidPeerZeroAddress(); - } - if (decimals == 0) { - revert InvalidPeerDecimals(); - } - if (peerChainId == chainId) { - revert InvalidPeerSameChainId(); - } - - NttManagerPeer memory oldPeer = _getPeersStorage()[peerChainId]; - - _getPeersStorage()[peerChainId].peerAddress = peerContract; - _getPeersStorage()[peerChainId].tokenDecimals = decimals; + ) public override onlyOwner { + super.setPeer(peerChainId, peerContract, decimals, inboundLimit); uint8 toDecimals = tokenDecimals(); _setInboundLimit(inboundLimit.trim(toDecimals, toDecimals), peerChainId); - - emit PeerUpdated( - peerChainId, oldPeer.peerAddress, oldPeer.tokenDecimals, peerContract, decimals - ); } /// @inheritdoc INttManager function setOutboundLimit( uint256 limit - ) external onlyOwner { + ) external override onlyOwner { uint8 toDecimals = tokenDecimals(); _setOutboundLimit(limit.trim(toDecimals, toDecimals)); } /// @inheritdoc INttManager - function setInboundLimit(uint256 limit, uint16 chainId_) external onlyOwner { + function setInboundLimit(uint256 limit, uint16 chainId_) external override onlyOwner { uint8 toDecimals = tokenDecimals(); _setInboundLimit(limit.trim(toDecimals, toDecimals), chainId_); } @@ -157,78 +99,28 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { // ==================== External Interface =============================================== - /// @inheritdoc INttManager - function transfer( - uint256 amount, - uint16 recipientChain, - bytes32 recipient - ) external payable nonReentrant whenNotPaused returns (uint64) { - return - _transferEntryPoint(amount, recipientChain, recipient, recipient, false, new bytes(1)); - } - - /// @inheritdoc INttManager - function transfer( - uint256 amount, - uint16 recipientChain, - bytes32 recipient, - bytes32 refundAddress, - bool shouldQueue, - bytes memory transceiverInstructions - ) external payable nonReentrant whenNotPaused returns (uint64) { - return _transferEntryPoint( - amount, recipientChain, recipient, refundAddress, shouldQueue, transceiverInstructions - ); - } - - /// @inheritdoc INttManager - function attestationReceived( - uint16 sourceChainId, - bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload - ) external onlyTransceiver whenNotPaused { - _verifyPeer(sourceChainId, sourceNttManagerAddress); - - // Compute manager message digest and record transceiver attestation. - bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); - - if (isMessageApproved(nttManagerMessageHash)) { - executeMsg(sourceChainId, sourceNttManagerAddress, payload); - } - } - /// @inheritdoc INttManager function executeMsg( uint16 sourceChainId, bytes32 sourceNttManagerAddress, TransceiverStructs.NttManagerMessage memory message - ) public whenNotPaused { - (bytes32 digest, bool alreadyExecuted) = - _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); - + ) public override whenNotPaused { + ( + bytes32 digest, + bool alreadyExecuted, + address transferRecipient, + TrimmedAmount nativeTransferAmount + ) = super._executeMsg1(sourceChainId, sourceNttManagerAddress, message); if (alreadyExecuted) { return; } - TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer = - TransceiverStructs.parseNativeTokenTransfer(message.payload); - - // verify that the destination chain is valid - if (nativeTokenTransfer.toChain != chainId) { - revert InvalidTargetChain(nativeTokenTransfer.toChain, chainId); - } - uint8 toDecimals = tokenDecimals(); - TrimmedAmount nativeTransferAmount = - (nativeTokenTransfer.amount.untrim(toDecimals)).trim(toDecimals, toDecimals); - - address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to); - { // Check inbound rate limits - bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId); + bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId); // BOINK if (isRateLimited) { // queue up the transfer - _enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient); + _enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient); // BOINK // end execution early return; @@ -236,20 +128,20 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { } // consume the amount for the inbound rate limit - _consumeInboundAmount(nativeTransferAmount, sourceChainId); + _consumeInboundAmount(nativeTransferAmount, sourceChainId); // BOINK // When receiving a transfer, we refill the outbound rate limit // by the same amount (we call this "backflow") - _backfillOutboundAmount(nativeTransferAmount); + _backfillOutboundAmount(nativeTransferAmount); // BOINK - _mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false); + super._executeMsg2(digest, transferRecipient, nativeTransferAmount); } /// @inheritdoc INttManager function completeInboundQueuedTransfer( bytes32 digest - ) external nonReentrant whenNotPaused { + ) external override nonReentrant whenNotPaused { // find the message in the queue - InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest); + InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest); // BOINK if (queuedTransfer.txTimestamp == 0) { revert InboundQueuedTransferNotFound(digest); } @@ -269,7 +161,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { /// @inheritdoc INttManager function completeOutboundQueuedTransfer( uint64 messageSequence - ) external payable nonReentrant whenNotPaused returns (uint64) { + ) external payable override nonReentrant whenNotPaused returns (uint64) { // find the message in the queue OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence]; if (queuedTransfer.txTimestamp == 0) { @@ -299,7 +191,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { /// @inheritdoc INttManager function cancelOutboundQueuedTransfer( uint64 messageSequence - ) external nonReentrant whenNotPaused { + ) external override nonReentrant whenNotPaused { // find the message in the queue OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence]; if (queuedTransfer.txTimestamp == 0) { @@ -329,64 +221,11 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { bytes32 refundAddress, bool shouldQueue, bytes memory transceiverInstructions - ) internal returns (uint64) { - if (amount == 0) { - revert ZeroAmount(); - } - - if (recipient == bytes32(0)) { - revert InvalidRecipient(); - } - - if (refundAddress == bytes32(0)) { - revert InvalidRefundAddress(); - } + ) internal override returns (uint64) { + (uint64 sequence, TrimmedAmount trimmedAmount) = + _transferEntryPoint1(amount, recipientChain, recipient, refundAddress); - { - // Lock/burn tokens before checking rate limits - // use transferFrom to pull tokens from the user and lock them - // query own token balance before transfer - uint256 balanceBefore = _getTokenBalanceOf(token, address(this)); - - // transfer tokens - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - - // query own token balance after transfer - uint256 balanceAfter = _getTokenBalanceOf(token, address(this)); - - // correct amount for potential transfer fees - amount = balanceAfter - balanceBefore; - if (mode == Mode.BURNING) { - { - // NOTE: We don't account for burn fees in this code path. - // We verify that the user's change in balance is equal to the amount that's burned. - // Accounting for burn fees can be non-trivial, since there - // is no standard way to account for the fee if the fee amount - // is taken out of the burn amount. - // For example, if there's a fee of 1 which is taken out of the - // amount, then burning 20 tokens would result in a transfer of only 19 tokens. - // However, the difference in the user's balance would only show 20. - // Since there is no standard way to query for burn fee amounts with burnable tokens, - // and NTT would be used on a per-token basis, implementing this functionality - // is left to integrating projects who may need to account for burn fees on their tokens. - ERC20Burnable(token).burn(amount); - - // tokens held by the contract after the operation should be the same as before - uint256 balanceAfterBurn = _getTokenBalanceOf(token, address(this)); - if (balanceBefore != balanceAfterBurn) { - revert BurnAmountDifferentThanBalanceDiff(balanceBefore, balanceAfterBurn); - } - } - } - } - - // 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); @@ -438,156 +277,12 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { ); } - function _transfer( - uint64 sequence, - TrimmedAmount amount, - uint16 recipientChain, - bytes32 recipient, - bytes32 refundAddress, - address sender, - bytes memory transceiverInstructions - ) internal returns (uint64 msgSequence) { - // verify chain has not forked - checkFork(evmChainId); - - ( - address[] memory enabledTransceivers, - TransceiverStructs.TransceiverInstruction[] memory instructions, - uint256[] memory priceQuotes, - uint256 totalPriceQuote - ) = _prepareForTransfer(recipientChain, transceiverInstructions); - - // push it on the stack again to avoid a stack too deep error - uint64 seq = sequence; - - TransceiverStructs.NativeTokenTransfer memory ntt = TransceiverStructs.NativeTokenTransfer( - amount, toWormholeFormat(token), recipient, recipientChain - ); - - // construct the NttManagerMessage payload - bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( - TransceiverStructs.NttManagerMessage( - bytes32(uint256(seq)), - toWormholeFormat(sender), - TransceiverStructs.encodeNativeTokenTransfer(ntt) - ) - ); - - // push onto the stack again to avoid stack too deep error - uint16 destinationChain = recipientChain; - - // send the message - _sendMessageToTransceivers( - recipientChain, - refundAddress, - _getPeersStorage()[destinationChain].peerAddress, - priceQuotes, - instructions, - enabledTransceivers, - encodedNttManagerPayload - ); - - // push it on the stack again to avoid a stack too deep error - TrimmedAmount amt = amount; - - emit TransferSent( - recipient, - refundAddress, - amt.untrim(tokenDecimals()), - totalPriceQuote, - destinationChain, - seq - ); - - // return the sequence number - return seq; - } - - function _mintOrUnlockToRecipient( - bytes32 digest, - address recipient, - TrimmedAmount amount, - bool cancelled - ) internal { - // verify chain has not forked - checkFork(evmChainId); - - // calculate proper amount of tokens to unlock/mint to recipient - // untrim the amount - uint256 untrimmedAmount = amount.untrim(tokenDecimals()); - - if (cancelled) { - emit OutboundTransferCancelled(uint256(digest), recipient, untrimmedAmount); - } else { - emit TransferRedeemed(digest); - } - - if (mode == Mode.LOCKING) { - // unlock tokens to the specified recipient - IERC20(token).safeTransfer(recipient, untrimmedAmount); - } else if (mode == Mode.BURNING) { - // mint tokens to the specified recipient - INttToken(token).mint(recipient, untrimmedAmount); - } else { - revert InvalidMode(uint8(mode)); - } - } - - function tokenDecimals() public view override(INttManager, RateLimiter) returns (uint8) { - (bool success, bytes memory queriedDecimals) = - token.staticcall(abi.encodeWithSignature("decimals()")); - - if (!success) { - revert StaticcallFailed(); - } - - return abi.decode(queriedDecimals, (uint8)); - } - - // ==================== Internal Helpers =============================================== - - /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. - function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view { - if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { - revert InvalidPeer(sourceChainId, peerAddress); - } - } - - function _trimTransferAmount( - uint256 amount, - uint16 toChain - ) internal view returns (TrimmedAmount) { - uint8 toDecimals = _getPeersStorage()[toChain].tokenDecimals; - - if (toDecimals == 0) { - revert InvalidPeerDecimals(); - } - - TrimmedAmount trimmedAmount; - { - uint8 fromDecimals = tokenDecimals(); - trimmedAmount = amount.trim(fromDecimals, toDecimals); - // don't deposit dust that can not be bridged due to the decimal shift - uint256 newAmount = trimmedAmount.untrim(fromDecimals); - if (amount != newAmount) { - revert TransferAmountHasDust(amount, amount - newAmount); - } - } - - return trimmedAmount; - } - - function _getTokenBalanceOf( - address tokenAddr, - address accountAddr - ) internal view returns (uint256) { - (bool success, bytes memory queriedBalance) = - tokenAddr.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, accountAddr)); - - if (!success) { - revert StaticcallFailed(); - } - - return abi.decode(queriedBalance, (uint256)); + function tokenDecimals() + public + view + override(NttManagerWithoutRateLimiting, RateLimiter) + returns (uint8) + { + return NttManagerWithoutRateLimiting.tokenDecimals(); } } diff --git a/evm/src/NttManager/NttManagerWithoutRateLimiting.sol b/evm/src/NttManager/NttManagerWithoutRateLimiting.sol new file mode 100644 index 000000000..85b1a9097 --- /dev/null +++ b/evm/src/NttManager/NttManagerWithoutRateLimiting.sol @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "../libraries/RateLimiter.sol"; + +import "../interfaces/INttManager.sol"; +import "../interfaces/INttToken.sol"; +import "../interfaces/ITransceiver.sol"; + +import {ManagerBase} from "./ManagerBase.sol"; + +/// @title NttManagerWithoutRateLimiting +/// @author Wormhole Project Contributors. +/// @notice The NttManager contract is responsible for managing the token +/// and associated transceivers. It is similar to the NttManager +/// but it does not use the rate limiter. +/// +/// @dev Each NttManager contract is associated with a single token but +/// can be responsible for multiple transceivers. +/// +/// @dev When transferring tokens, the NttManager contract will either +/// lock the tokens or burn them, depending on the mode. +/// +/// @dev To initiate a transfer, the user calls the transfer function with: +/// - the amount +/// - the recipient chain +/// - the recipient address +/// - the refund address: the address to which refunds are issued for any unused gas +/// for attestations on a given transfer. If the gas limit is configured +/// to be too high, users will be refunded the difference. +/// - (optional) a flag to indicate whether the transfer should be queued +/// if the rate limit is exceeded +contract NttManagerWithoutRateLimiting is INttManager, ManagerBase { + using BytesParsing for bytes; + using SafeERC20 for IERC20; + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + string public constant NTT_MANAGER_VERSION = "1.1.0"; + + // =============== Setup ================================================================= + + constructor(address _token, Mode _mode, uint16 _chainId) ManagerBase(_token, _mode, _chainId) {} + + function __NttManager_init() internal virtual onlyInitializing { + // check if the owner is the deployer of this contract + if (msg.sender != deployer) { + revert UnexpectedDeployer(deployer, msg.sender); + } + if (msg.value != 0) { + revert UnexpectedMsgValue(); + } + __PausedOwnable_init(msg.sender, msg.sender); + __ReentrancyGuard_init(); + } + + function _initialize() internal virtual override { + __NttManager_init(); + _checkThresholdInvariants(); + _checkTransceiversInvariants(); + } + + // =============== Storage ============================================================== + + bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("ntt.peers")) - 1); + + // =============== Storage Getters/Setters ============================================== + + function _getPeersStorage() + internal + pure + returns (mapping(uint16 => NttManagerPeer) storage $) + { + uint256 slot = uint256(PEERS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + // =============== Public Getters ======================================================== + + /// @inheritdoc INttManager + function getPeer( + uint16 chainId_ + ) external view returns (NttManagerPeer memory) { + return _getPeersStorage()[chainId_]; + } + + // =============== Admin ============================================================== + + /// @inheritdoc INttManager + function setPeer( + uint16 peerChainId, + bytes32 peerContract, + uint8 decimals, + uint256 /*inboundLimit*/ + ) public virtual onlyOwner { + if (peerChainId == 0) { + revert InvalidPeerChainIdZero(); + } + if (peerContract == bytes32(0)) { + revert InvalidPeerZeroAddress(); + } + if (decimals == 0) { + revert InvalidPeerDecimals(); + } + if (peerChainId == chainId) { + revert InvalidPeerSameChainId(); + } + + NttManagerPeer memory oldPeer = _getPeersStorage()[peerChainId]; + + _getPeersStorage()[peerChainId].peerAddress = peerContract; + _getPeersStorage()[peerChainId].tokenDecimals = decimals; + + emit PeerUpdated( + peerChainId, oldPeer.peerAddress, oldPeer.tokenDecimals, peerContract, decimals + ); + } + + /// @inheritdoc INttManager + function setOutboundLimit( + uint256 /*limit*/ + ) external virtual onlyOwner { + revert NotImplemented(); + } + + /// @inheritdoc INttManager + function setInboundLimit(uint256, /*limit*/ uint16 /*chainId*/ ) external virtual onlyOwner { + revert NotImplemented(); + } + + /// ============== Invariants ============================================= + + /// @dev When we add new immutables, this function should be updated + function _checkImmutables() internal view virtual override { + super._checkImmutables(); + } + + // ==================== External Interface =============================================== + + /// @inheritdoc INttManager + function transfer( + uint256 amount, + uint16 recipientChain, + bytes32 recipient + ) external payable nonReentrant whenNotPaused returns (uint64) { + return + _transferEntryPoint(amount, recipientChain, recipient, recipient, false, new bytes(1)); + } + + /// @inheritdoc INttManager + function transfer( + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + bytes32 refundAddress, + bool shouldQueue, + bytes memory transceiverInstructions + ) external payable nonReentrant whenNotPaused returns (uint64) { + return _transferEntryPoint( + amount, recipientChain, recipient, refundAddress, shouldQueue, transceiverInstructions + ); + } + + /// @inheritdoc INttManager + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory payload + ) external onlyTransceiver whenNotPaused { + _verifyPeer(sourceChainId, sourceNttManagerAddress); + + // Compute manager message digest and record transceiver attestation. + bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); + + if (isMessageApproved(nttManagerMessageHash)) { + executeMsg(sourceChainId, sourceNttManagerAddress, payload); + } + } + + /// @inheritdoc INttManager + function executeMsg( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory message + ) public virtual whenNotPaused { + ( + bytes32 digest, + bool alreadyExecuted, + address transferRecipient, + TrimmedAmount nativeTransferAmount + ) = _executeMsg1(sourceChainId, sourceNttManagerAddress, message); + if (alreadyExecuted) { + return; + } + + _executeMsg2(digest, transferRecipient, nativeTransferAmount); + } + + /// @inheritdoc INttManager + function completeInboundQueuedTransfer( + bytes32 /*digest*/ + ) external virtual nonReentrant whenNotPaused { + revert NotImplemented(); + } + + /// @inheritdoc INttManager + function completeOutboundQueuedTransfer( + uint64 /*messageSequence*/ + ) external payable virtual nonReentrant whenNotPaused returns (uint64) { + revert NotImplemented(); + } + + /// @inheritdoc INttManager + function cancelOutboundQueuedTransfer( + uint64 /*messageSequence*/ + ) external virtual nonReentrant whenNotPaused { + revert NotImplemented(); + } + + // ==================== Internal Business Logic ========================================= + + function _executeMsg1( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory message + ) + internal + returns ( + bytes32 digest, + bool alreadyExecuted, + address transferRecipient, + TrimmedAmount nativeTransferAmount + ) + { + (digest, alreadyExecuted) = + _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); + + if (alreadyExecuted) { + return (digest, alreadyExecuted, transferRecipient, nativeTransferAmount); + } + + TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer = + TransceiverStructs.parseNativeTokenTransfer(message.payload); + + // verify that the destination chain is valid + if (nativeTokenTransfer.toChain != chainId) { + revert InvalidTargetChain(nativeTokenTransfer.toChain, chainId); + } + uint8 toDecimals = tokenDecimals(); + nativeTransferAmount = + (nativeTokenTransfer.amount.untrim(toDecimals)).trim(toDecimals, toDecimals); + + transferRecipient = fromWormholeFormat(nativeTokenTransfer.to); + } + + function _executeMsg2( + bytes32 digest, + address transferRecipient, + TrimmedAmount nativeTransferAmount + ) internal { + _mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false); + } + + function _transferEntryPoint( + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + bytes32 refundAddress, + bool, /*shouldQueue*/ + bytes memory transceiverInstructions + ) internal virtual returns (uint64) { + (uint64 sequence, TrimmedAmount trimmedAmount) = + _transferEntryPoint1(amount, recipientChain, recipient, refundAddress); + + return _transfer( + sequence, + trimmedAmount, + recipientChain, + recipient, + refundAddress, + msg.sender, + transceiverInstructions + ); + } + + function _transferEntryPoint1( + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + bytes32 refundAddress + ) internal returns (uint64 sequence, TrimmedAmount trimmedAmount) { + if (amount == 0) { + revert ZeroAmount(); + } + + if (recipient == bytes32(0)) { + revert InvalidRecipient(); + } + + if (refundAddress == bytes32(0)) { + revert InvalidRefundAddress(); + } + + { + // Lock/burn tokens before checking rate limits + // use transferFrom to pull tokens from the user and lock them + // query own token balance before transfer + uint256 balanceBefore = _getTokenBalanceOf(token, address(this)); + + // transfer tokens + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + // query own token balance after transfer + uint256 balanceAfter = _getTokenBalanceOf(token, address(this)); + + // correct amount for potential transfer fees + amount = balanceAfter - balanceBefore; + if (mode == Mode.BURNING) { + { + // NOTE: We don't account for burn fees in this code path. + // We verify that the user's change in balance is equal to the amount that's burned. + // Accounting for burn fees can be non-trivial, since there + // is no standard way to account for the fee if the fee amount + // is taken out of the burn amount. + // For example, if there's a fee of 1 which is taken out of the + // amount, then burning 20 tokens would result in a transfer of only 19 tokens. + // However, the difference in the user's balance would only show 20. + // Since there is no standard way to query for burn fee amounts with burnable tokens, + // and NTT would be used on a per-token basis, implementing this functionality + // is left to integrating projects who may need to account for burn fees on their tokens. + ERC20Burnable(token).burn(amount); + + // tokens held by the contract after the operation should be the same as before + uint256 balanceAfterBurn = _getTokenBalanceOf(token, address(this)); + if (balanceBefore != balanceAfterBurn) { + revert BurnAmountDifferentThanBalanceDiff(balanceBefore, balanceAfterBurn); + } + } + } + } + + // trim amount after burning to ensure transfer amount matches (amount - fee) + trimmedAmount = _trimTransferAmount(amount, recipientChain); + + // get the sequence for this transfer + sequence = _useMessageSequence(); + } + + function _transfer( + uint64 sequence, + TrimmedAmount amount, + uint16 recipientChain, + bytes32 recipient, + bytes32 refundAddress, + address sender, + bytes memory transceiverInstructions + ) internal returns (uint64 msgSequence) { + // verify chain has not forked + checkFork(evmChainId); + + ( + address[] memory enabledTransceivers, + TransceiverStructs.TransceiverInstruction[] memory instructions, + uint256[] memory priceQuotes, + uint256 totalPriceQuote + ) = _prepareForTransfer(recipientChain, transceiverInstructions); + + // push it on the stack again to avoid a stack too deep error + uint64 seq = sequence; + + TransceiverStructs.NativeTokenTransfer memory ntt = TransceiverStructs.NativeTokenTransfer( + amount, toWormholeFormat(token), recipient, recipientChain + ); + + // construct the NttManagerMessage payload + bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( + TransceiverStructs.NttManagerMessage( + bytes32(uint256(seq)), + toWormholeFormat(sender), + TransceiverStructs.encodeNativeTokenTransfer(ntt) + ) + ); + + // push onto the stack again to avoid stack too deep error + uint16 destinationChain = recipientChain; + + // send the message + _sendMessageToTransceivers( + recipientChain, + refundAddress, + _getPeersStorage()[destinationChain].peerAddress, + priceQuotes, + instructions, + enabledTransceivers, + encodedNttManagerPayload + ); + + // push it on the stack again to avoid a stack too deep error + TrimmedAmount amt = amount; + + emit TransferSent( + recipient, + refundAddress, + amt.untrim(tokenDecimals()), + totalPriceQuote, + destinationChain, + seq + ); + + // return the sequence number + return seq; + } + + function _mintOrUnlockToRecipient( + bytes32 digest, + address recipient, + TrimmedAmount amount, + bool cancelled + ) internal { + // verify chain has not forked + checkFork(evmChainId); + + // calculate proper amount of tokens to unlock/mint to recipient + // untrim the amount + uint256 untrimmedAmount = amount.untrim(tokenDecimals()); + + if (cancelled) { + emit OutboundTransferCancelled(uint256(digest), recipient, untrimmedAmount); + } else { + emit TransferRedeemed(digest); + } + + if (mode == Mode.LOCKING) { + // unlock tokens to the specified recipient + IERC20(token).safeTransfer(recipient, untrimmedAmount); + } else if (mode == Mode.BURNING) { + // mint tokens to the specified recipient + INttToken(token).mint(recipient, untrimmedAmount); + } else { + revert InvalidMode(uint8(mode)); + } + } + + function tokenDecimals() public view virtual override(INttManager) returns (uint8) { + (bool success, bytes memory queriedDecimals) = + token.staticcall(abi.encodeWithSignature("decimals()")); + + if (!success) { + revert StaticcallFailed(); + } + + return abi.decode(queriedDecimals, (uint8)); + } + + // ==================== Internal Helpers =============================================== + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view { + if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { + revert InvalidPeer(sourceChainId, peerAddress); + } + } + + function _trimTransferAmount( + uint256 amount, + uint16 toChain + ) internal view returns (TrimmedAmount) { + uint8 toDecimals = _getPeersStorage()[toChain].tokenDecimals; + + if (toDecimals == 0) { + revert InvalidPeerDecimals(); + } + + TrimmedAmount trimmedAmount; + { + uint8 fromDecimals = tokenDecimals(); + trimmedAmount = amount.trim(fromDecimals, toDecimals); + // don't deposit dust that can not be bridged due to the decimal shift + uint256 newAmount = trimmedAmount.untrim(fromDecimals); + if (amount != newAmount) { + revert TransferAmountHasDust(amount, amount - newAmount); + } + } + + return trimmedAmount; + } + + function _getTokenBalanceOf( + address tokenAddr, + address accountAddr + ) internal view returns (uint256) { + (bool success, bytes memory queriedBalance) = + tokenAddr.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, accountAddr)); + + if (!success) { + revert StaticcallFailed(); + } + + return abi.decode(queriedBalance, (uint256)); + } +} diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index c17de81a3..aeb5ea883 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -135,6 +135,9 @@ interface INttManager is IManagerBase { /// @dev Selector 0x20371f2a. error InvalidPeerSameChainId(); + /// @notice Feature is not implemented. + error NotImplemented(); + /// @notice Transfer a given amount to a recipient on a given chain. This function is called /// by the user to send the token cross-chain. This function will either lock or burn the /// sender's tokens. Finally, this function will call into registered `Endpoint` contracts diff --git a/evm/test/IntegrationWithoutRateLimiting.t.sol b/evm/test/IntegrationWithoutRateLimiting.t.sol new file mode 100755 index 000000000..49fe580ca --- /dev/null +++ b/evm/test/IntegrationWithoutRateLimiting.t.sol @@ -0,0 +1,743 @@ +// 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/NttManagerWithoutRateLimiting.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 { + NttManagerWithoutRateLimiting nttManagerChain1; + NttManagerWithoutRateLimiting 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(); + NttManagerWithoutRateLimiting implementation = new MockNttManagerWithoutRateLimitingContract( + address(t1), IManagerBase.Mode.LOCKING, chainId1, 1 days, false + ); + + nttManagerChain1 = MockNttManagerWithoutRateLimitingContract( + 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(); + NttManagerWithoutRateLimiting implementationChain2 = new MockNttManagerWithoutRateLimitingContract( + address(t2), IManagerBase.Mode.BURNING, chainId2, 1 days, false + ); + + nttManagerChain2 = MockNttManagerWithoutRateLimitingContract( + 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, + "NttManagerWithoutRateLimiting 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, + "NttManagerWithoutRateLimiting 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"); + } + } + + function test_someReverts() public { + 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(INttManager.NotImplemented.selector)); + nttManagerChain1.setOutboundLimit(0); + + vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + nttManagerChain1.setInboundLimit(0, chainId2); + + vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + nttManagerChain1.completeInboundQueuedTransfer(bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(INttManager.NotImplemented.selector)); + nttManagerChain1.completeOutboundQueuedTransfer(0); + + vm.expectRevert(abi.encodeWithSelector(INttManager.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, + "NttManagerWithoutRateLimiting 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) + ); + + /* + // Test timing on the queues + vm.expectRevert( + abi.encodeWithSelector( + IRateLimiter.OutboundQueuedTransferStillQueued.selector, + 0, + vm.getBlockTimestamp() + ) + ); + nttManagerChain2.completeOutboundQueuedTransfer(0); + vm.warp(vm.getBlockTimestamp() + 1 days - 1); + + vm.expectRevert( + abi.encodeWithSelector( + IRateLimiter.OutboundQueuedTransferStillQueued.selector, + 0, + vm.getBlockTimestamp() - 1 days + 1 + ) + ); + nttManagerChain2.completeOutboundQueuedTransfer(0); + + vm.warp(vm.getBlockTimestamp() + 1); + nttManagerChain2.completeOutboundQueuedTransfer(0); + + // Replay - should be deleted + vm.expectRevert( + abi.encodeWithSelector(IRateLimiter.OutboundQueuedTransferNotFound.selector, 0) + ); + nttManagerChain2.completeOutboundQueuedTransfer(0); + + // Non-existant + vm.expectRevert( + abi.encodeWithSelector(IRateLimiter.OutboundQueuedTransferNotFound.selector, 1) + ); + nttManagerChain2.completeOutboundQueuedTransfer(1); + */ + + 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, + "NttManagerWithoutRateLimiting 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"); + + // vm.warp(vm.getBlockTimestamp() + 100000); + // nttManagerChain1.completeInboundQueuedTransfer(queuedDigests[0]); + + // // Double redeem + // vm.warp(vm.getBlockTimestamp() + 100000); + // vm.expectRevert( + // abi.encodeWithSelector( + // IRateLimiter.InboundQueuedTransferNotFound.selector, queuedDigests[0] + // ) + // ); + // nttManagerChain1.completeInboundQueuedTransfer(queuedDigests[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 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]); + + 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, + "NttManagerWithoutRateLimiting 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, + "NttManagerWithoutRateLimiting 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/mocks/MockNttManager.sol b/evm/test/mocks/MockNttManager.sol index 6adfcaa2c..bf142ecf6 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/NttManagerWithoutRateLimiting.sol"; contract MockNttManagerContract is NttManager { constructor( @@ -26,6 +27,28 @@ contract MockNttManagerContract is NttManager { } } +contract MockNttManagerWithoutRateLimitingContract is NttManagerWithoutRateLimiting { + constructor( + address token, + Mode mode, + uint16 chainId, + uint64 rateLimitDuration, + bool skipRateLimiting + ) NttManagerWithoutRateLimiting(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(