From e453a76d2d930979f17738964c90ac674e3387e3 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Mon, 25 Nov 2024 21:33:43 -0800 Subject: [PATCH 01/10] feat: smart M bridging --- script/deploy/DeployBase.sol | 15 +- script/helpers/Utils.sol | 1 + script/upgrade/UpgradeBase.sol | 17 +- src/HubPortal.sol | 48 ++-- src/Portal.sol | 228 ++++++++++++++++-- src/SpokePortal.sol | 10 +- src/governance/Migrator.sol | 15 +- src/interfaces/IPortal.sol | 115 ++++++++- src/interfaces/IWrappedMTokenLike.sol | 21 ++ src/libs/PayloadEncoder.sol | 32 ++- test/fork/Configure.t.sol | 2 + test/fork/Migrate.t.sol | 1 + test/fork/fixtures/deploy-config.json | 1 + .../fixtures/migrator/MainnetMigrator.sol | 6 + .../fixtures/migrator/SepoliaMigrator.sol | 6 + test/fork/fixtures/upgrade-config.json | 1 + test/harnesses/PortalHarness.sol | 3 +- test/mocks/MockWrappedMToken.sol | 30 +++ test/unit/HubPortal.t.sol | 10 +- test/unit/Portal.t.sol | 20 +- test/unit/SpokePortal.t.sol | 10 +- test/unit/UnitTestBase.t.sol | 2 +- test/unit/libs/PayloadEncoder.t.sol | 57 +++-- 23 files changed, 559 insertions(+), 92 deletions(-) create mode 100644 src/interfaces/IWrappedMTokenLike.sol create mode 100644 test/mocks/MockWrappedMToken.sol diff --git a/script/deploy/DeployBase.sol b/script/deploy/DeployBase.sol index 6524ceb..73c58e7 100644 --- a/script/deploy/DeployBase.sol +++ b/script/deploy/DeployBase.sol @@ -42,6 +42,7 @@ contract DeployBase is Script, Utils { struct HubConfiguration { address mToken; + address smartMToken; address registrar; WormholeConfiguration wormhole; } @@ -132,7 +133,12 @@ contract DeployBase is Script, Utils { } function _deployHubPortal(address deployer_, HubConfiguration memory config_) internal returns (address) { - HubPortal implementation_ = new HubPortal(config_.mToken, config_.registrar, config_.wormhole.chainId); + HubPortal implementation_ = new HubPortal( + config_.mToken, + config_.smartMToken, + config_.registrar, + config_.wormhole.chainId + ); HubPortal hubPortalProxy_ = HubPortal( _deployCreate3Proxy(address(implementation_), _computeSalt(deployer_, "Portal")) ); @@ -150,7 +156,10 @@ contract DeployBase is Script, Utils { address registrar_, uint16 wormholeChainId_ ) internal returns (address) { - SpokePortal implementation_ = new SpokePortal(mToken_, registrar_, wormholeChainId_); + // Pre-compute the expected SpokeSmartMToken proxy address. + address expectedSmartMTokenProxy_ = ContractHelper.getContractFrom(deployer_, _SPOKE_SMART_M_TOKEN_PROXY_NONCE); + + SpokePortal implementation_ = new SpokePortal(mToken_, expectedSmartMTokenProxy_, registrar_, wormholeChainId_); SpokePortal spokePortalProxy_ = SpokePortal( _deployCreate3Proxy(address(implementation_), _computeSalt(deployer_, "Portal")) ); @@ -340,9 +349,11 @@ contract DeployBase is Script, Utils { console.log("Hub configuration for chain ID %s loaded:", chainId_); hubConfig_.mToken = file_.readAddress(_readKey(hub_, "m_token")); + hubConfig_.smartMToken = file_.readAddress(_readKey(hub_, "smart_m_token")); hubConfig_.registrar = file_.readAddress(_readKey(hub_, "registrar")); console.log("M Token:", hubConfig_.mToken); + console.log("Smart M Token:", hubConfig_.smartMToken); console.log("Registrar:", hubConfig_.registrar); hubConfig_.wormhole = _loadWormholeConfig(file_, hub_); diff --git a/script/helpers/Utils.sol b/script/helpers/Utils.sol index aad70f2..c323dc8 100644 --- a/script/helpers/Utils.sol +++ b/script/helpers/Utils.sol @@ -20,6 +20,7 @@ contract Utils { address internal constant _MAINNET_REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _MAINNET_M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + address internal constant _MAINNET_SMART_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; address internal constant _MAINNET_VAULT = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; address internal constant _SEPOLIA_REGISTRAR = 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A; diff --git a/script/upgrade/UpgradeBase.sol b/script/upgrade/UpgradeBase.sol index b7066db..a61d9bd 100644 --- a/script/upgrade/UpgradeBase.sol +++ b/script/upgrade/UpgradeBase.sol @@ -24,6 +24,7 @@ contract UpgradeBase is Script, Utils { struct PortalConfiguration { address mToken; + address smartMToken; address registrar; address portal; uint16 wormholeChainId; @@ -57,7 +58,12 @@ contract UpgradeBase is Script, Utils { } function _upgradeHubPortal(PortalConfiguration memory config_) internal { - HubPortal implementation_ = new HubPortal(config_.mToken, config_.registrar, config_.wormholeChainId); + HubPortal implementation_ = new HubPortal( + config_.mToken, + config_.smartMToken, + config_.registrar, + config_.wormholeChainId + ); console.log("HubPortal implementation deployed at: ", address(implementation_)); @@ -65,7 +71,12 @@ contract UpgradeBase is Script, Utils { } function _upgradeSpokePortal(PortalConfiguration memory config_) internal { - SpokePortal implementation_ = new SpokePortal(config_.mToken, config_.registrar, config_.wormholeChainId); + SpokePortal implementation_ = new SpokePortal( + config_.mToken, + config_.smartMToken, + config_.registrar, + config_.wormholeChainId + ); console.log("SpokePortal implementation deployed at: ", address(implementation_)); @@ -84,11 +95,13 @@ contract UpgradeBase is Script, Utils { console.log("Portal configuration for chain ID %s loaded:", chainId_); portalConfig_.mToken = file_.readAddress(_readKey(config_, "m_token")); + portalConfig_.smartMToken = file_.readAddress(_readKey(config_, "smart_m_token")); portalConfig_.registrar = file_.readAddress(_readKey(config_, "registrar")); portalConfig_.portal = file_.readAddress(_readKey(config_, "portal")); portalConfig_.wormholeChainId = uint16(file_.readUint(_readKey(config_, "wormhole.chain_id"))); console.log("M Token:", portalConfig_.mToken); + console.log("Smart M Token:", portalConfig_.smartMToken); console.log("Registrar:", portalConfig_.registrar); console.log("Portal:", portalConfig_.portal); console.log("Wormhole chain ID:", portalConfig_.wormholeChainId); diff --git a/src/HubPortal.sol b/src/HubPortal.sol index 135e606..39925bb 100644 --- a/src/HubPortal.sol +++ b/src/HubPortal.sol @@ -20,9 +20,6 @@ import { TypeConverter } from "./libs/TypeConverter.sol"; contract HubPortal is IHubPortal, Portal { using TypeConverter for address; - /// @dev Use only standard WormholeTransceiver with relaying enabled - bytes public constant DEFAULT_TRANSCEIVER_INSTRUCTIONS = new bytes(1); - /* ============ Variables ============ */ /// @inheritdoc IHubPortal @@ -35,15 +32,17 @@ contract HubPortal is IHubPortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param smartMToken_ The address of the Smart M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, + address smartMToken_, address registrar_, uint16 chainId_ - ) Portal(mToken_, registrar_, Mode.LOCKING, chainId_) {} + ) Portal(mToken_, smartMToken_, registrar_, Mode.LOCKING, chainId_) {} /* ============ Interactive Functions ============ */ @@ -54,7 +53,7 @@ contract HubPortal is IHubPortal, Portal { ) external payable returns (bytes32 messageId_) { uint128 index_ = _currentIndex(); bytes memory payload_ = PayloadEncoder.encodeIndex(index_, destinationChainId_); - messageId_ = _sendMessage(destinationChainId_, refundAddress_, payload_); + messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_); emit MTokenIndexSent(destinationChainId_, messageId_, index_); } @@ -67,7 +66,7 @@ contract HubPortal is IHubPortal, Portal { ) external payable returns (bytes32 messageId_) { bytes32 value_ = IRegistrarLike(registrar).get(key_); bytes memory payload_ = PayloadEncoder.encodeKey(key_, value_, destinationChainId_); - messageId_ = _sendMessage(destinationChainId_, refundAddress_, payload_); + messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_); emit RegistrarKeySent(destinationChainId_, messageId_, key_, value_); } @@ -81,7 +80,7 @@ contract HubPortal is IHubPortal, Portal { ) external payable returns (bytes32 messageId_) { bool status_ = IRegistrarLike(registrar).listContains(listName_, account_); bytes memory payload_ = PayloadEncoder.encodeListUpdate(listName_, account_, status_, destinationChainId_); - messageId_ = _sendMessage(destinationChainId_, refundAddress_, payload_); + messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_); emit RegistrarListStatusSent(destinationChainId_, messageId_, listName_, account_, status_); } @@ -118,43 +117,28 @@ contract HubPortal is IHubPortal, Portal { * @param amount_ The amount of M Token to unlock to the recipient. */ function _mintOrUnlock(address recipient_, uint256 amount_, uint128) internal override { - IERC20(mToken()).transfer(recipient_, amount_); + if (recipient_ != address(this)) { + IERC20(mToken()).transfer(recipient_, amount_); + } } - /// @notice Sends a generic message to the destination chain. - /// @dev The implementation is adapted from `NttManager` `_transfer` function. - function _sendMessage( + /// @dev Sends a custom (not a transfer) message to the destination chain. + function _sendCustomMessage( uint16 destinationChainId_, bytes32 refundAddress_, bytes memory payload_ ) private returns (bytes32 messageId_) { if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); - ( - address[] memory enabledTransceivers_, - TransceiverStructs.TransceiverInstruction[] memory instructions_, - uint256[] memory priceQuotes_, - - ) = _prepareForTransfer(destinationChainId_, DEFAULT_TRANSCEIVER_INSTRUCTIONS); - TransceiverStructs.NttManagerMessage memory message_ = TransceiverStructs.NttManagerMessage( bytes32(uint256(_useMessageSequence())), msg.sender.toBytes32(), payload_ ); - // send the message - _sendMessageToTransceivers( - destinationChainId_, - refundAddress_, - _getPeersStorage()[destinationChainId_].peerAddress, - priceQuotes_, - instructions_, - enabledTransceivers_, - TransceiverStructs.encodeNttManagerMessage(message_) - ); + _sendMessage(destinationChainId_, refundAddress_, message_); - return TransceiverStructs.nttManagerMessageDigest(chainId, message_); + messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_); } /* ============ Internal View/Pure Functions ============ */ diff --git a/src/Portal.sol b/src/Portal.sol index 0986da8..5caa10f 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; +import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol"; import { TrimmedAmount, TrimmedAmountLib @@ -12,6 +13,7 @@ import { } from "../lib/example-native-token-transfers/evm/src/NttManager/NttManagerNoRateLimiting.sol"; import { IPortal } from "./interfaces/IPortal.sol"; +import { IWrappedMTokenLike } from "./interfaces/IWrappedMTokenLike.sol"; import { TypeConverter } from "./libs/TypeConverter.sol"; import { PayloadType, PayloadEncoder } from "./libs/PayloadEncoder.sol"; @@ -24,25 +26,39 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { using PayloadEncoder for bytes; using TrimmedAmountLib for *; + /// @dev Use only standard WormholeTransceiver with relaying enabled + bytes public constant DEFAULT_TRANSCEIVER_INSTRUCTIONS = new bytes(1); + + bytes32 constant EMPTY_WRAPPER_ADDRESS = bytes32(0); + /// @inheritdoc IPortal address public immutable registrar; + /// @inheritdoc IPortal + address public immutable smartMToken; + + /// @inheritdoc IPortal + mapping(uint16 remoteChainId => bytes32 smartMToken) public remoteSmartMToken; + /* ============ Constructor ============ */ /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. - * @param chainId_ The Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param smartMToken_ The address of the Smart M token to bridge. + * @param registrar_ The address of the Registrar. + * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. + * @param chainId_ The Wormhole chain id. */ constructor( address mToken_, + address smartMToken_, address registrar_, Mode mode_, uint16 chainId_ ) NttManagerNoRateLimiting(mToken_, mode_, chainId_) { if (mToken_ == address(0)) revert ZeroMToken(); + if ((smartMToken = smartMToken_) == address(0)) revert ZeroSmartMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); } @@ -58,9 +74,53 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { return _currentIndex(); } - /* ============ Internal Interactive Functions ============ */ + /* ============ External Interactive Functions ============ */ + + /// @inheritdoc IPortal + function setRemoteSmartMToken(uint16 remoteChainId_, bytes32 smartMToken_) external onlyOwner { + remoteSmartMToken[remoteChainId_] = smartMToken_; + emit RemoteSmartMTokenSet(remoteChainId_, smartMToken_); + } + + /// @inheritdoc IPortal + function transferSmartMToken( + uint256 amount_, + uint16 destinationChainId_, + bytes32 recipient_, + bytes32 refundAddress_ + ) external payable returns (bytes32 messageId_) { + messageId_ = _transferWrappedMToken( + amount_, + smartMToken, + remoteSmartMToken[destinationChainId_], + destinationChainId_, + recipient_, + refundAddress_ + ); + } + + /// @inheritdoc IPortal + function transferWrappedMToken( + uint256 amount_, + address sourceWrappedToken_, + bytes32 destinationWrappedToken_, + uint16 destinationChainId_, + bytes32 recipient_, + bytes32 refundAddress_ + ) external payable returns (bytes32 messageId_) { + messageId_ = _transferWrappedMToken( + amount_, + sourceWrappedToken_, + destinationWrappedToken_, + destinationChainId_, + recipient_, + refundAddress_ + ); + } + /* ============ Internal/Private Interactive Functions ============ */ - /// @dev Adds M Token index to the NTT payload. + /// @dev Called from NTT manager during M Token transfer to customize additional payload. + /// Adds M Token index and empty Wrapper Address to the NTT payload. function _prepareNativeTokenTransfer( TrimmedAmount amount_, bytes32 recipient_, @@ -69,29 +129,128 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { address sender_, bytes32 // refundAddress ) internal override returns (TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_) { - // Convert to uint64 for compatibility with Solana and other non-EVM chains. - uint64 index_ = _currentIndex().toUint64(); + uint128 index_ = _currentIndex(); + bytes32 messageId_; + (nativeTokenTransfer_, , messageId_) = _encodeTokenTransfer( + amount_, + index_, + recipient_, + EMPTY_WRAPPER_ADDRESS, + destinationChainId_, + sequence_, + sender_ + ); + + emit MTokenSent(destinationChainId_, messageId_, sender_, recipient_, amount_.untrim(tokenDecimals()), index_); + } + function _encodeTokenTransfer( + TrimmedAmount amount_, + uint128 index_, + bytes32 recipient_, + bytes32 destinationWrappedToken_, + uint16 destinationChainId_, + uint64 sequence_, + address sender_ + ) + internal + returns ( + TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_, + TransceiverStructs.NttManagerMessage memory message_, + bytes32 messageId_ + ) + { nativeTokenTransfer_ = TransceiverStructs.NativeTokenTransfer( amount_, token.toBytes32(), recipient_, destinationChainId_, - abi.encodePacked(index_) + PayloadEncoder.encodeAdditionalPayload(index_, destinationWrappedToken_) ); - bytes32 messageId_ = TransceiverStructs.nttManagerMessageDigest( - chainId, - TransceiverStructs.NttManagerMessage( - bytes32(uint256(sequence_)), - sender_.toBytes32(), - TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_) - ) + message_ = TransceiverStructs.NttManagerMessage( + bytes32(uint256(sequence_)), + sender_.toBytes32(), + TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_) ); - uint256 untrimmedAmount_ = amount_.untrim(tokenDecimals()); + messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_); + } + + /// @dev Transfers a Wrapped M token to the destination chain by unwrapping it to M Token + function _transferWrappedMToken( + uint256 amount_, + address sourceWrappedToken_, + bytes32 destinationWrappedToken_, + uint16 destinationChainId_, + bytes32 recipient_, + bytes32 refundAddress_ + ) private returns (bytes32 messageId_) { + // transfer Wrapped M from the sender + IERC20(sourceWrappedToken_).transferFrom(msg.sender, address(this), amount_); + + // unwrap Wrapped M token to M Token + amount_ = IWrappedMTokenLike(sourceWrappedToken_).unwrap(address(this), amount_); + + // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. + // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. + + if (amount_ == 0) revert ZeroAmount(); + if (recipient_ == bytes32(0)) revert InvalidRecipient(); + if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); + + // get the sequence for this transfer + uint64 sequence_ = _useMessageSequence(); + uint128 index_ = _currentIndex(); + + TransceiverStructs.NttManagerMessage memory message_; + (, message_, messageId_) = _encodeTokenTransfer( + _trimTransferAmount(amount_, destinationChainId_), + index_, + recipient_, + destinationWrappedToken_, + destinationChainId_, + sequence_, + msg.sender + ); + + uint256 totalPriceQuote_ = _sendMessage(destinationChainId_, refundAddress_, message_); + + emit MTokenSent(destinationChainId_, messageId_, msg.sender, recipient_, amount_, index_); - emit MTokenSent(destinationChainId_, messageId_, sender_, recipient_, untrimmedAmount_, index_); + // Emit NTT events + emit TransferSent(recipient_, refundAddress_, amount_, totalPriceQuote_, destinationChainId_, sequence_); + emit TransferSent(messageId_); + } + + /// @notice Sends a generic message to the destination chain. + /// @dev The implementation is adapted from `NttManager` `_transfer` function. + function _sendMessage( + uint16 destinationChainId_, + bytes32 refundAddress_, + TransceiverStructs.NttManagerMessage memory message_ + ) internal returns (uint256) { + _verifyIfChainForked(); + + ( + address[] memory enabledTransceivers_, + TransceiverStructs.TransceiverInstruction[] memory instructions_, + uint256[] memory priceQuotes_, + uint256 totalPriceQuote_ + ) = _prepareForTransfer(destinationChainId_, DEFAULT_TRANSCEIVER_INSTRUCTIONS); + + // send a message + _sendMessageToTransceivers( + destinationChainId_, + refundAddress_, + _getPeersStorage()[destinationChainId_].peerAddress, + priceQuotes_, + instructions_, + enabledTransceivers_, + TransceiverStructs.encodeNttManagerMessage(message_) + ); + + return totalPriceQuote_; } /// @dev Handles token transfer with an additional payload and custom payload types on the destination. @@ -115,8 +274,13 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { } function _receiveMToken(uint16 sourceChainId_, bytes32 messageId_, bytes32 sender_, bytes memory payload_) private { - (TrimmedAmount trimmedAmount_, uint128 index_, address recipient_, uint16 destinationChainId_) = payload_ - .decodeTokenTransfer(); + ( + TrimmedAmount trimmedAmount_, + uint128 index_, + address destinationWrappedToken_, + address recipient_, + uint16 destinationChainId_ + ) = payload_.decodeTokenTransfer(); _verifyDestinationChain(destinationChainId_); @@ -128,7 +292,29 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { // Emitting `INttManager.TransferRedeemed` to comply with Wormhole NTT specification. emit TransferRedeemed(messageId_); - _mintOrUnlock(recipient_, amount_, index_); + if (destinationWrappedToken_ == address(0)) { + // mints or unlocks M Token to the recipient + _mintOrUnlock(recipient_, amount_, index_); + } else { + // mints or unlocks M Token to the Portal + _mintOrUnlock(address(this), amount_, index_); + + // wraps M token and transfers it to the recipient + _wrap(destinationWrappedToken_, recipient_, amount_); + } + } + + /// @dev Wraps M token to the token specified by `destinationWrappedToken_`. + /// If wrapping fails transfers M token to `recipient_`. + function _wrap(address destinationWrappedToken_, address recipient_, uint256 amount_) private { + (bool success, ) = destinationWrappedToken_.call( + abi.encodeCall(IWrappedMTokenLike.wrap, (recipient_, amount_)) + ); + + if (!success) { + emit WrapFailed(destinationWrappedToken_, recipient_, amount_); + IERC20(mToken()).transfer(recipient_, amount_); + } } function _receiveCustomPayload( diff --git a/src/SpokePortal.sol b/src/SpokePortal.sol index 2aa2b0c..8d41b6d 100644 --- a/src/SpokePortal.sol +++ b/src/SpokePortal.sol @@ -21,15 +21,17 @@ contract SpokePortal is ISpokePortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param smartMToken_ The address of the Smart M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, + address smartMToken_, address registrar_, uint16 chainId_ - ) Portal(mToken_, registrar_, Mode.BURNING, chainId_) {} + ) Portal(mToken_, smartMToken_, registrar_, Mode.BURNING, chainId_) {} /* ============ Internal/Private Interactive Functions ============ */ diff --git a/src/governance/Migrator.sol b/src/governance/Migrator.sol index ad5c4d3..3f962ca 100644 --- a/src/governance/Migrator.sol +++ b/src/governance/Migrator.sol @@ -23,6 +23,7 @@ abstract contract Migrator is IMigrator { /// @dev Portal migration parameters. struct PortalMigrateParams { address mToken; + address smartMToken; address registrar; uint16 wormholeChainId; } @@ -66,7 +67,12 @@ abstract contract Migrator is IMigrator { * @param params_ The parameters for the migrate. */ function _migrateHubPortal(PortalMigrateParams memory params_) internal { - HubPortal implementation_ = new HubPortal(params_.mToken, params_.registrar, params_.wormholeChainId); + HubPortal implementation_ = new HubPortal( + params_.mToken, + params_.smartMToken, + params_.registrar, + params_.wormholeChainId + ); IManagerBase(portal).upgrade(address(implementation_)); } @@ -75,7 +81,12 @@ abstract contract Migrator is IMigrator { * @param params_ The parameters for the migrate. */ function _migrateSpokePortal(PortalMigrateParams memory params_) internal { - SpokePortal implementation_ = new SpokePortal(params_.mToken, params_.registrar, params_.wormholeChainId); + SpokePortal implementation_ = new SpokePortal( + params_.mToken, + params_.smartMToken, + params_.registrar, + params_.wormholeChainId + ); IManagerBase(portal).upgrade(address(implementation_)); } diff --git a/src/interfaces/IPortal.sol b/src/interfaces/IPortal.sol index 75358a1..576ec66 100644 --- a/src/interfaces/IPortal.sol +++ b/src/interfaces/IPortal.sol @@ -10,7 +10,7 @@ interface IPortal { /* ============ Events ============ */ /** - * @notice Emitted when M token are sent to a destination chain. + * @notice Emitted when M token is sent to a destination chain. * @param destinationChainId The Wormhole destination chain ID. * @param messageId The unique identifier for the sent message. * @param sender The address that bridged the M tokens via the Portal. @@ -28,7 +28,29 @@ interface IPortal { ); /** - * @notice Emitted when M token are received from a source chain. + * @notice Emitted when Wrapped M token is sent to a destination chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param sourceWrappedToken The address of Wrapped M Token on the source chain. + * @param destinationWrappedToken The address of Wrapped M Token on the destination chain. + * @param messageId The unique identifier for the sent message. + * @param sender The address that bridged the M tokens via the Portal. + * @param recipient The account receiving tokens on destination chain. + * @param amount The amount of tokens. + * @param index The the M token index. + */ + event WrappedMTokenSent( + uint16 destinationChainId, + address indexed sourceWrappedToken, + bytes32 destinationWrappedToken, + bytes32 messageId, + address indexed sender, + bytes32 indexed recipient, + uint256 amount, + uint128 index + ); + + /** + * @notice Emitted when M token is received from a source chain. * @param sourceChainId The Wormhole source chain ID. * @param messageId The unique identifier for the received message. * @param sender The account sending tokens. @@ -45,11 +67,49 @@ interface IPortal { uint128 index ); + /** + * @notice Emitted when Wrapped M token is received from a source chain. + * @param sourceChainId The Wormhole source chain ID. + * @param destinationWrappedToken The address of the Wrapped M Token on the destination chain. + * @param messageId The unique identifier for the received message. + * @param sender The account sending tokens. + * @param recipient The account receiving tokens. + * @param amount The amount of tokens. + * @param index The the M token index. + */ + event WrappedMTokenReceived( + uint16 sourceChainId, + address indexed destinationWrappedToken, + bytes32 messageId, + bytes32 indexed sender, + address indexed recipient, + uint256 amount, + uint128 index + ); + + /** + * @notice Emitted when wrapping M token is failed on the destination. + * @param destinationWrappedToken The address of the Wrapped M Token on the destination chain. + * @param recipient The account receiving tokens. + * @param amount The amount of tokens. + */ + event WrapFailed(address indexed destinationWrappedToken, address indexed recipient, uint256 amount); + + /** + * @notice Emitted when Smart M token is set for the remote chain. + * @param remoteChainId The Wormhole remote chain ID. + * @param smartMToken The address of the Smart M Token on the remote chain. + */ + event RemoteSmartMTokenSet(uint16 remoteChainId, bytes32 smartMToken); + /* ============ Custom Errors ============ */ /// @notice Emitted when the M token is 0x0. error ZeroMToken(); + /// @notice Emitted when the Smart M token is 0x0. + error ZeroSmartMToken(); + /// @notice Emitted when the Registrar address is 0x0. error ZeroRegistrar(); @@ -67,4 +127,55 @@ interface IPortal { /// @notice The address of the Registrar contract. function registrar() external view returns (address); + + /// @notice The address of the Smart M token. + function smartMToken() external view returns (address); + + /** + * @notice Returns the address of the Smart M Token on the remote chain. + * @param remoteChainId The Wormhole remote chain ID. + * @return smartMToken address on the remote chain. + */ + function remoteSmartMToken(uint16 remoteChainId) external view returns (bytes32 smartMToken); + + /* ============ Interactive Functions ============ */ + + /// @notice Sets the address of Smart M Token on the remote chain. + function setRemoteSmartMToken(uint16 remoteChainId, bytes32 smartMToken) external; + + /** + * @notice Transfers Smart M Token to the destination chain. + * @param amount The amount of tokens to transfer. + * @param destinationChainId The Wormhole destination chain ID. + * @param recipient The account to receive tokens. + * @param refundAddress The address to receive excess native gas on the destination chain. + * @return messageId The ID uniquely identifying the message. + */ + function transferSmartMToken( + uint256 amount, + uint16 destinationChainId, + bytes32 recipient, + bytes32 refundAddress + ) external payable returns (bytes32 messageId); + + /** + * @notice Transfers Wrapped M Token to the destination chain. + * @dev Can be used for transferring M Token Extensions and converting between different Wrappers. + * @param amount The amount of tokens to transfer. + * @param sourceWrappedToken The address of the Wrapped M Token of the source chain. + * @param destinationWrappedToken The address of the Wrapped M Token of the destination chain. + * @param amount The amount of tokens to transfer. + * @param destinationChainId The Wormhole destination chain ID. + * @param recipient The account to receive tokens. + * @param refundAddress The address to receive excess native gas on the destination chain. + * @return messageId The ID uniquely identifying the message. + */ + function transferWrappedMToken( + uint256 amount, + address sourceWrappedToken, + bytes32 destinationWrappedToken, + uint16 destinationChainId, + bytes32 recipient, + bytes32 refundAddress + ) external payable returns (bytes32 messageId); } diff --git a/src/interfaces/IWrappedMTokenLike.sol b/src/interfaces/IWrappedMTokenLike.sol new file mode 100644 index 0000000..d0c988e --- /dev/null +++ b/src/interfaces/IWrappedMTokenLike.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +interface IWrappedMTokenLike { + /** + * @notice Wraps `amount` M from the caller into wM for `recipient`. + * @param recipient The account receiving the minted wM. + * @param amount The amount of M deposited. + * @return wrapped The amount of wM minted. + */ + function wrap(address recipient, uint256 amount) external returns (uint240 wrapped); + + /** + * @notice Unwraps `amount` wM from the caller into M for `recipient`. + * @param recipient The account receiving the withdrawn M. + * @param amount The amount of wM burned. + * @return unwrapped The amount of M withdrawn. + */ + function unwrap(address recipient, uint256 amount) external returns (uint240 unwrapped); +} diff --git a/src/libs/PayloadEncoder.sol b/src/libs/PayloadEncoder.sol index ff5be88..7e39506 100644 --- a/src/libs/PayloadEncoder.sol +++ b/src/libs/PayloadEncoder.sol @@ -47,20 +47,44 @@ library PayloadEncoder { ) internal pure - returns (TrimmedAmount trimmedAmount_, uint128 index_, address recipient_, uint16 destinationChainId_) + returns ( + TrimmedAmount trimmedAmount_, + uint128 index_, + address destinationWrappedToken_, + address recipient_, + uint16 destinationChainId_ + ) { TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_ = TransceiverStructs .parseNativeTokenTransfer(payload_); - uint256 offset_ = 0; - (index_, offset_) = nativeTokenTransfer_.additionalPayload.asUint64Unchecked(offset_); - nativeTokenTransfer_.additionalPayload.checkLength(offset_); + (index_, destinationWrappedToken_) = decodeAdditionalPayload(nativeTokenTransfer_.additionalPayload); trimmedAmount_ = nativeTokenTransfer_.amount; recipient_ = nativeTokenTransfer_.to.toAddress(); destinationChainId_ = nativeTokenTransfer_.toChain; } + function encodeAdditionalPayload( + uint128 index_, + bytes32 destinationWrappedToken_ + ) internal pure returns (bytes memory encoded_) { + return abi.encodePacked(index_.toUint64(), destinationWrappedToken_); + } + + function decodeAdditionalPayload( + bytes memory payload_ + ) internal pure returns (uint128 index_, address destinationWrappedToken_) { + uint256 offset_ = 0; + bytes32 token_; + + (index_, offset_) = payload_.asUint64Unchecked(offset_); + (token_, offset_) = payload_.asBytes32Unchecked(offset_); + destinationWrappedToken_ = token_.toAddress(); + + payload_.checkLength(offset_); + } + function encodeIndex(uint128 index_, uint16 destinationChainId_) internal pure returns (bytes memory encoded_) { return abi.encodePacked(INDEX_TRANSFER_PREFIX, index_.toUint64(), destinationChainId_); } diff --git a/test/fork/Configure.t.sol b/test/fork/Configure.t.sol index 3f119e9..e3c2d70 100644 --- a/test/fork/Configure.t.sol +++ b/test/fork/Configure.t.sol @@ -42,6 +42,7 @@ contract Configure is ForkTestBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, + _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); @@ -136,6 +137,7 @@ contract Configure is ForkTestBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, + _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); diff --git a/test/fork/Migrate.t.sol b/test/fork/Migrate.t.sol index f1cdf9f..9625ca0 100644 --- a/test/fork/Migrate.t.sol +++ b/test/fork/Migrate.t.sol @@ -60,6 +60,7 @@ contract Migrate is ForkTestBase, UpgradeBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, + _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); diff --git a/test/fork/fixtures/deploy-config.json b/test/fork/fixtures/deploy-config.json index c2cb506..ac5ff4a 100644 --- a/test/fork/fixtures/deploy-config.json +++ b/test/fork/fixtures/deploy-config.json @@ -2,6 +2,7 @@ "hub": { "1": { "m_token": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", + "smart_m_token": "0x437cc33344a0B27A429f795ff6B469C72698B291", "registrar": "0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c", "wormhole": { "chain_id": "2", diff --git a/test/fork/fixtures/migrator/MainnetMigrator.sol b/test/fork/fixtures/migrator/MainnetMigrator.sol index 6fe6ed5..a855798 100644 --- a/test/fork/fixtures/migrator/MainnetMigrator.sol +++ b/test/fork/fixtures/migrator/MainnetMigrator.sol @@ -22,6 +22,9 @@ contract MainnetMigrator is Migrator { /// @dev Mainnet MToken address. address internal constant _MAINNET_M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + /// @dev Mainnet Smart MToken address. + address internal constant _MAINNET_SMART_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; + /// @dev Mainnet Registrar address. address internal constant _MAINNET_REGISTRAR = 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A; @@ -49,6 +52,7 @@ contract MainnetMigrator is Migrator { _migrateHubPortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, + smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _MAINNET_WORMHOLE_CHAIN_ID }) @@ -68,6 +72,7 @@ contract MainnetMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, + smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _BASE_WORMHOLE_CHAIN_ID }) @@ -87,6 +92,7 @@ contract MainnetMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, + smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _OPTIMISM_WORMHOLE_CHAIN_ID }) diff --git a/test/fork/fixtures/migrator/SepoliaMigrator.sol b/test/fork/fixtures/migrator/SepoliaMigrator.sol index 62a14b6..fa65e81 100644 --- a/test/fork/fixtures/migrator/SepoliaMigrator.sol +++ b/test/fork/fixtures/migrator/SepoliaMigrator.sol @@ -22,6 +22,9 @@ contract SepoliaMigrator is Migrator { /// @dev Sepolia Spoke M token address. address internal constant _SEPOLIA_SPOKE_M_TOKEN = 0xCEC6566b227a95C76a0E3dbFdC7794CA795C7F9e; + /// @dev Sepolia Spoke Smart M token address. + address internal constant _SEPOLIA_SPOKE_SMART_M_TOKEN = 0xCEC6566b227a95C76a0E3dbFdC7794CA795C7F9e; + /// @dev Sepolia Spoke Registrar address. address internal constant _SEPOLIA_SPOKE_REGISTRAR = 0x39a5F8C5ADC500E1d30115c09A1016764D90bC94; @@ -49,6 +52,7 @@ contract SepoliaMigrator is Migrator { _migrateHubPortal( PortalMigrateParams({ mToken: 0x0c941AD94Ca4A52EDAeAbF203b61bdd1807CeEC0, + smartMToken: 0x437cc33344a0B27A429f795ff6B469C72698B291, registrar: 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A, wormholeChainId: _SEPOLIA_WORMHOLE_CHAIN_ID }) @@ -68,6 +72,7 @@ contract SepoliaMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _SEPOLIA_SPOKE_M_TOKEN, + smartMToken: _SEPOLIA_SPOKE_SMART_M_TOKEN, registrar: _SEPOLIA_SPOKE_REGISTRAR, wormholeChainId: _BASE_SEPOLIA_WORMHOLE_CHAIN_ID }) @@ -87,6 +92,7 @@ contract SepoliaMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _SEPOLIA_SPOKE_M_TOKEN, + smartMToken: _SEPOLIA_SPOKE_SMART_M_TOKEN, registrar: _SEPOLIA_SPOKE_REGISTRAR, wormholeChainId: _OPTIMISM_SEPOLIA_WORMHOLE_CHAIN_ID }) diff --git a/test/fork/fixtures/upgrade-config.json b/test/fork/fixtures/upgrade-config.json index d37c296..3194a90 100644 --- a/test/fork/fixtures/upgrade-config.json +++ b/test/fork/fixtures/upgrade-config.json @@ -2,6 +2,7 @@ "config": { "1": { "m_token": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", + "smart_m_token": "0x437cc33344a0B27A429f795ff6B469C72698B291", "registrar": "0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c", "portal": "0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd", "wormhole": { diff --git a/test/harnesses/PortalHarness.sol b/test/harnesses/PortalHarness.sol index fd87449..9d6da2f 100644 --- a/test/harnesses/PortalHarness.sol +++ b/test/harnesses/PortalHarness.sol @@ -7,8 +7,9 @@ import { Portal } from "../../src/Portal.sol"; contract PortalHarness is Portal { constructor( address mToken_, + address smartMToken_, address registrar_, Mode mode_, uint16 chainId_ - ) Portal(mToken_, registrar_, mode_, chainId_) {} + ) Portal(mToken_, smartMToken_, registrar_, mode_, chainId_) {} } diff --git a/test/mocks/MockWrappedMToken.sol b/test/mocks/MockWrappedMToken.sol new file mode 100644 index 0000000..b7376b5 --- /dev/null +++ b/test/mocks/MockWrappedMToken.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; + +import { MockERC20 } from "./MockERC20.sol"; +import { console } from "../../lib/forge-std/src/console.sol"; + +contract MockWrappedMToken is MockERC20 { + address public mToken; + + constructor(address mToken_) MockERC20("Mock Wrapped M", "Wrapped M", 6) { + mToken = mToken_; + } + + function wrap(address recipient_, uint256 amount_) external returns (uint240 wrapped_) { + uint256 startingBalance_ = IERC20(mToken).balanceOf(address(this)); + IERC20(mToken).transferFrom(msg.sender, address(this), amount_); + wrapped_ = uint240(IERC20(mToken).balanceOf(address(this)) - startingBalance_); + _mint(recipient_, wrapped_); + } + + function unwrap(address recipient_, uint256 amount_) external returns (uint240 unwrapped_) { + _burn(msg.sender, amount_); + uint256 startingBalance_ = IERC20(mToken).balanceOf(address(this)); + IERC20(mToken).transfer(recipient_, amount_); + return uint240(startingBalance_ - IERC20(mToken).balanceOf(address(this))); + } +} diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index 2c4ad63..ffdda56 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -14,6 +14,7 @@ import { TypeConverter } from "../../src/libs/TypeConverter.sol"; import { UnitTestBase } from "./UnitTestBase.t.sol"; import { MockHubMToken } from "../mocks/MockHubMToken.sol"; +import { MockWrappedMToken } from "../mocks/MockWrappedMToken.sol"; import { MockHubRegistrar } from "../mocks/MockHubRegistrar.sol"; import { MockTransceiver } from "../mocks/MockTransceiver.sol"; @@ -21,12 +22,14 @@ contract HubPortalTests is UnitTestBase { using TypeConverter for *; MockHubMToken internal _mToken; + MockWrappedMToken internal _smartMToken; MockHubRegistrar internal _registrar; HubPortal internal _portal; function setUp() external { _mToken = new MockHubMToken(); + _smartMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -34,7 +37,12 @@ contract HubPortalTests is UnitTestBase { _registrar = new MockHubRegistrar(); _transceiver = new MockTransceiver(); - HubPortal implementation_ = new HubPortal(address(_mToken), address(_registrar), _LOCAL_CHAIN_ID); + HubPortal implementation_ = new HubPortal( + address(_mToken), + address(_smartMToken), + address(_registrar), + _LOCAL_CHAIN_ID + ); _portal = HubPortal(_createProxy(address(implementation_))); _initializePortal(_portal); diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index 51ef005..7e014f1 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -12,6 +12,7 @@ import { TypeConverter } from "../../src/libs/TypeConverter.sol"; import { PayloadEncoder } from "../../src/libs/PayloadEncoder.sol"; import { UnitTestBase } from "./UnitTestBase.t.sol"; +import { MockWrappedMToken } from "../mocks/MockWrappedMToken.sol"; import { MockSpokeMToken } from "../mocks/MockSpokeMToken.sol"; import { MockTransceiver } from "../mocks/MockTransceiver.sol"; import { MockSpokeRegistrar } from "../mocks/MockSpokeRegistrar.sol"; @@ -22,12 +23,14 @@ contract PortalTests is UnitTestBase { using TrimmedAmountLib for *; MockSpokeMToken internal _mToken; + MockWrappedMToken internal _smartMToken; MockSpokeRegistrar internal _registrar; PortalHarness internal _portal; function setUp() external { _mToken = new MockSpokeMToken(); + _smartMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -37,6 +40,7 @@ contract PortalTests is UnitTestBase { PortalHarness implementation_ = new PortalHarness( address(_mToken), + address(_smartMToken), address(_registrar), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID @@ -49,12 +53,24 @@ contract PortalTests is UnitTestBase { function test_constructor_zeroMToken() external { vm.expectRevert(IPortal.ZeroMToken.selector); - new PortalHarness(address(0), address(_registrar), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID); + new PortalHarness( + address(0), + address(_smartMToken), + address(_registrar), + IManagerBase.Mode.BURNING, + _LOCAL_CHAIN_ID + ); } function test_constructor_zeroRegistrar() external { vm.expectRevert(IPortal.ZeroRegistrar.selector); - new PortalHarness(address(_mToken), address(0), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID); + new PortalHarness( + address(_mToken), + address(_smartMToken), + address(0), + IManagerBase.Mode.BURNING, + _LOCAL_CHAIN_ID + ); } /* ============ transfer ============ */ diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index 01cccd4..51636f1 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -13,6 +13,7 @@ import { PayloadEncoder } from "../../src/libs/PayloadEncoder.sol"; import { TypeConverter } from "../../src/libs/TypeConverter.sol"; import { UnitTestBase } from "./UnitTestBase.t.sol"; +import { MockWrappedMToken } from "../mocks/MockWrappedMToken.sol"; import { MockSpokeMToken } from "../mocks/MockSpokeMToken.sol"; import { MockSpokeRegistrar } from "../mocks/MockSpokeRegistrar.sol"; import { MockTransceiver } from "../mocks/MockTransceiver.sol"; @@ -21,12 +22,14 @@ contract SpokePortalTests is UnitTestBase { using TypeConverter for *; MockSpokeMToken internal _mToken; + MockWrappedMToken internal _smartMToken; MockSpokeRegistrar internal _registrar; SpokePortal internal _portal; function setUp() external { _mToken = new MockSpokeMToken(); + _smartMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -34,7 +37,12 @@ contract SpokePortalTests is UnitTestBase { _registrar = new MockSpokeRegistrar(); _transceiver = new MockTransceiver(); - SpokePortal implementation_ = new SpokePortal(address(_mToken), address(_registrar), _LOCAL_CHAIN_ID); + SpokePortal implementation_ = new SpokePortal( + address(_mToken), + address(_smartMToken), + address(_registrar), + _LOCAL_CHAIN_ID + ); _portal = SpokePortal(_createProxy(address(implementation_))); _initializePortal(_portal); diff --git a/test/unit/UnitTestBase.t.sol b/test/unit/UnitTestBase.t.sol index e5a59fe..eb9c670 100644 --- a/test/unit/UnitTestBase.t.sol +++ b/test/unit/UnitTestBase.t.sol @@ -78,7 +78,7 @@ contract UnitTestBase is Test { _tokenAddress.toBytes32(), recipient_, destinationChainId_, - abi.encodePacked(index_.toUint64()) + abi.encodePacked(index_.toUint64(), bytes32(0)) ); bytes memory payload_ = TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_); message_ = TransceiverStructs.NttManagerMessage(bytes32(0), _alice.toBytes32(), payload_); diff --git a/test/unit/libs/PayloadEncoder.t.sol b/test/unit/libs/PayloadEncoder.t.sol index dd2c986..215eb10 100644 --- a/test/unit/libs/PayloadEncoder.t.sol +++ b/test/unit/libs/PayloadEncoder.t.sol @@ -70,33 +70,54 @@ contract PayloadEncoderTest is Test { assertEq(uint8(PayloadEncoder.getPayloadType(payload_)), uint8(PayloadType.List)); } - function test_decodeTokenTransfer_invalidAdditionalPayloadLength() external { - uint256 amount_ = 1000; - uint8 index_ = 1; + function test_encodeAdditionalPayload() external { + uint128 index_ = 1e12; + bytes32 wrappedToken_ = makeAddr("wrapped").toBytes32(); + bytes memory payload_ = abi.encodePacked(uint64(index_), wrappedToken_); - bytes memory payload_ = TransceiverStructs.encodeNativeTokenTransfer( - TransceiverStructs.NativeTokenTransfer( - amount_.trim(_TOKEN_DECIMALS, _TOKEN_DECIMALS), - _token.toBytes32(), - _recipient.toBytes32(), - _DESTINATION_CHAIN_ID, - abi.encodePacked(index_) // index isn't converted to uint64 - ) - ); + assertEq(PayloadEncoder.encodeAdditionalPayload(index_, wrappedToken_), payload_); + } + + function test_decodeAdditionalPayload() external { + uint128 encodedIndex_ = 1e12; + address encodedWrappedToken_ = makeAddr("wrapped"); + + bytes memory payload_ = abi.encodePacked(uint64(encodedIndex_), encodedWrappedToken_.toBytes32()); + + (uint128 decodedIndex_, address decodedWrappedToken_) = PayloadEncoder.decodeAdditionalPayload(payload_); + + assertEq(decodedIndex_, encodedIndex_); + assertEq(decodedWrappedToken_, encodedWrappedToken_); + } + + function testFuzz_decodeAdditionalPayload(uint64 encodedIndex_, address encodedWrappedToken_) external { + bytes memory payload_ = abi.encodePacked(encodedIndex_, encodedWrappedToken_.toBytes32()); + + (uint128 decodedIndex_, address decodedWrappedToken_) = PayloadEncoder.decodeAdditionalPayload(payload_); + + assertEq(decodedIndex_, encodedIndex_); + assertEq(decodedWrappedToken_, encodedWrappedToken_); + } + + function test_decodeAdditionalPayload_invalidLength() external { + uint128 index_ = 1e12; + // wrapped token isn't added to the payload + bytes memory payload_ = abi.encodePacked(uint64(index_)); - vm.expectRevert(abi.encodeWithSelector(BytesParsing.LengthMismatch.selector, 1, 8)); - this.decodeTransfer(payload_); + vm.expectRevert(abi.encodeWithSelector(BytesParsing.LengthMismatch.selector, 8, 40)); + this.decodeAdditionalPayload(payload_); } /// @dev a wrapper to prevent internal library functions from getting inlined /// https://github.com/foundry-rs/foundry/issues/7757 - function decodeTransfer(bytes memory payload_) public pure { - PayloadEncoder.decodeTokenTransfer(payload_); + function decodeAdditionalPayload(bytes memory payload_) public pure { + PayloadEncoder.decodeAdditionalPayload(payload_); } function test_decodeTokenTransfer() external { uint256 encodedAmount_ = 1000; uint128 encodedIndex_ = 1e12; + address encodedWrappedToken_ = makeAddr("wrapped"); bytes memory payload_ = TransceiverStructs.encodeNativeTokenTransfer( TransceiverStructs.NativeTokenTransfer( @@ -104,13 +125,14 @@ contract PayloadEncoderTest is Test { _token.toBytes32(), _recipient.toBytes32(), _DESTINATION_CHAIN_ID, - abi.encodePacked(uint64(encodedIndex_)) + abi.encodePacked(uint64(encodedIndex_), encodedWrappedToken_.toBytes32()) ) ); ( TrimmedAmount decodedTrimmedAmount_, uint128 decodedIndex_, + address decodedWrappedToken_, address decodedRecipient_, uint16 decodedDestinationChainId_ ) = PayloadEncoder.decodeTokenTransfer(payload_); @@ -119,6 +141,7 @@ contract PayloadEncoderTest is Test { assertEq(decodedAmount_, encodedAmount_); assertEq(decodedIndex_, encodedIndex_); + assertEq(decodedWrappedToken_, encodedWrappedToken_); assertEq(decodedRecipient_, _recipient); assertEq(decodedDestinationChainId_, _DESTINATION_CHAIN_ID); } From e6e21e2f80989095beb1b5fbc512fb6f3613c54c Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Tue, 26 Nov 2024 22:07:16 -0800 Subject: [PATCH 02/10] test: add unit tests --- src/Portal.sol | 13 +-- src/libs/SafeCall.sol | 11 +++ test/unit/Portal.t.sol | 161 ++++++++++++++++++++++++++++++++++- test/unit/SpokePortal.t.sol | 62 ++++++++++++++ test/unit/UnitTestBase.t.sol | 20 ++++- 5 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 src/libs/SafeCall.sol diff --git a/src/Portal.sol b/src/Portal.sol index 5caa10f..2521a0b 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -15,6 +15,7 @@ import { import { IPortal } from "./interfaces/IPortal.sol"; import { IWrappedMTokenLike } from "./interfaces/IWrappedMTokenLike.sol"; import { TypeConverter } from "./libs/TypeConverter.sol"; +import { SafeCall } from "./libs/SafeCall.sol"; import { PayloadType, PayloadEncoder } from "./libs/PayloadEncoder.sol"; /** @@ -25,6 +26,7 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { using TypeConverter for *; using PayloadEncoder for bytes; using TrimmedAmountLib for *; + using SafeCall for address; /// @dev Use only standard WormholeTransceiver with relaying enabled bytes public constant DEFAULT_TRANSCEIVER_INSTRUCTIONS = new bytes(1); @@ -186,6 +188,10 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { bytes32 recipient_, bytes32 refundAddress_ ) private returns (bytes32 messageId_) { + if (amount_ == 0) revert ZeroAmount(); + if (recipient_ == bytes32(0)) revert InvalidRecipient(); + if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); + // transfer Wrapped M from the sender IERC20(sourceWrappedToken_).transferFrom(msg.sender, address(this), amount_); @@ -195,11 +201,6 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. - if (amount_ == 0) revert ZeroAmount(); - if (recipient_ == bytes32(0)) revert InvalidRecipient(); - if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); - - // get the sequence for this transfer uint64 sequence_ = _useMessageSequence(); uint128 index_ = _currentIndex(); @@ -307,7 +308,7 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /// @dev Wraps M token to the token specified by `destinationWrappedToken_`. /// If wrapping fails transfers M token to `recipient_`. function _wrap(address destinationWrappedToken_, address recipient_, uint256 amount_) private { - (bool success, ) = destinationWrappedToken_.call( + bool success = destinationWrappedToken_.safeCall( abi.encodeCall(IWrappedMTokenLike.wrap, (recipient_, amount_)) ); diff --git a/src/libs/SafeCall.sol b/src/libs/SafeCall.sol new file mode 100644 index 0000000..d6f7706 --- /dev/null +++ b/src/libs/SafeCall.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +library SafeCall { + function safeCall(address target, bytes memory data) internal returns (bool success) { + if (target.code.length > 0) { + (success, ) = target.call(data); + } + } +} diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index 7e014f1..da5f50d 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -75,14 +75,14 @@ contract PortalTests is UnitTestBase { /* ============ transfer ============ */ - function test_transfer_insufficientAmount() external { + function test_transfer_zeroAmount() external { vm.expectRevert(INttManager.ZeroAmount.selector); vm.prank(_alice); _portal.transfer(0, _REMOTE_CHAIN_ID, _alice.toBytes32()); } - function test_transfer_invalidRecipient() external { + function test_transfer_zeroRecipient() external { vm.expectRevert(INttManager.InvalidRecipient.selector); vm.prank(_alice); @@ -142,6 +142,163 @@ contract PortalTests is UnitTestBase { _portal.transfer{ value: msgValue_ }(amount_, _REMOTE_CHAIN_ID, recipient_); } + /* ====== _transferWrappedMToken ====== */ + + function test_transferWrappedMToken_zeroAmount() external { + uint256 amount_ = 0; + bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + vm.expectRevert(INttManager.ZeroAmount.selector); + _portal.transferWrappedMToken( + amount_, + address(_smartMToken), + destinationWrappedToken_, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + } + + function test_transferWrappedMToken_zeroRecipient() external { + uint256 amount_ = 1_000e6; + bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); + bytes32 recipient_ = bytes32(0); + bytes32 refundAddress_ = _alice.toBytes32(); + + vm.expectRevert(INttManager.InvalidRecipient.selector); + _portal.transferWrappedMToken( + amount_, + address(_smartMToken), + destinationWrappedToken_, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + } + + function test_transferWrappedMToken_zeroRefundAddress() external { + uint256 amount_ = 1_000e6; + bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = bytes32(0); + + vm.expectRevert(INttManager.InvalidRefundAddress.selector); + _portal.transferWrappedMToken( + amount_, + address(_smartMToken), + destinationWrappedToken_, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + } + + function test_transferWrappedMToken() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 destinationWrappedToken_ = makeAddr("wrapped M").toBytes32(); + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + destinationWrappedToken_ + ); + + _mToken.mint(_alice, amount_); + + vm.startPrank(_alice); + _mToken.approve(address(_smartMToken), amount_); + amount_ = _smartMToken.wrap(_alice, amount_); + _smartMToken.approve(address(_portal), amount_); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent(_REMOTE_CHAIN_ID, messageId_, _alice, recipient_, amount_, index_); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferWrappedMToken( + amount_, + address(_smartMToken), + destinationWrappedToken_, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + } + + function test_transferSmartMToken() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 destinationSmartMToken_ = makeAddr("smart M").toBytes32(); + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + destinationSmartMToken_ + ); + + _mToken.mint(_alice, amount_); + _portal.setRemoteSmartMToken(_REMOTE_CHAIN_ID, destinationSmartMToken_); + + vm.startPrank(_alice); + _mToken.approve(address(_smartMToken), amount_); + amount_ = _smartMToken.wrap(_alice, amount_); + _smartMToken.approve(address(_portal), amount_); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent(_REMOTE_CHAIN_ID, messageId_, _alice, recipient_, amount_, index_); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferSmartMToken(amount_, _REMOTE_CHAIN_ID, recipient_, refundAddress_); + } + /* ============ _handleMsg ============ */ function test_handleMsg_invalidFork() external { diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index 51636f1..6d61f88 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -323,4 +323,66 @@ contract SpokePortalTests is UnitTestBase { vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); } + + function test_receiveWrappedMToken() external { + uint256 amount_ = 1_000e6; + uint128 localIndex_ = 1_100000068703; + uint128 remoteIndex_ = _EXP_SCALED_ONE; + + _mToken.setCurrentIndex(localIndex_); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + amount_, + remoteIndex_, + _alice.toBytes32(), + _REMOTE_CHAIN_ID, + _LOCAL_CHAIN_ID, + address(_smartMToken).toBytes32() + ); + + vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", address(_portal), amount_)); + vm.expectCall(address(_smartMToken), abi.encodeWithSignature("wrap(address,uint256)", _alice, amount_)); + + vm.expectEmit(); + emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + + vm.expectEmit(); + emit INttManager.TransferRedeemed(messageId_); + + vm.prank(address(_transceiver)); + _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); + } + + function test_receiveWrappedMToken_unwrapFails() external { + uint256 amount_ = 1_000e6; + uint128 localIndex_ = 1_100000068703; + uint128 remoteIndex_ = _EXP_SCALED_ONE; + address destinationWrappedToken_ = makeAddr("invalid"); + + _mToken.setCurrentIndex(localIndex_); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + amount_, + remoteIndex_, + _alice.toBytes32(), + _REMOTE_CHAIN_ID, + _LOCAL_CHAIN_ID, + destinationWrappedToken_.toBytes32() + ); + + vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", address(_portal), amount_)); + vm.expectCall(address(_mToken), abi.encodeWithSignature("transfer(address,uint256)", _alice, amount_)); + + vm.expectEmit(); + emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + + vm.expectEmit(); + emit INttManager.TransferRedeemed(messageId_); + + vm.expectEmit(); + emit IPortal.WrapFailed(destinationWrappedToken_, _alice, amount_); + + vm.prank(address(_transceiver)); + _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); + } } diff --git a/test/unit/UnitTestBase.t.sol b/test/unit/UnitTestBase.t.sol index eb9c670..65fbf60 100644 --- a/test/unit/UnitTestBase.t.sol +++ b/test/unit/UnitTestBase.t.sol @@ -72,13 +72,31 @@ contract UnitTestBase is Test { bytes32 recipient_, uint16 sourceChainId_, uint16 destinationChainId_ + ) internal view returns (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) { + (message_, messageId_) = _createWrappedMTransferMessage( + amount_, + index_, + recipient_, + sourceChainId_, + destinationChainId_, + bytes32(0) + ); + } + + function _createWrappedMTransferMessage( + uint256 amount_, + uint128 index_, + bytes32 recipient_, + uint16 sourceChainId_, + uint16 destinationChainId_, + bytes32 destinationToken_ ) internal view returns (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) { TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_ = TransceiverStructs.NativeTokenTransfer( amount_.trim(_tokenDecimals, _tokenDecimals), _tokenAddress.toBytes32(), recipient_, destinationChainId_, - abi.encodePacked(index_.toUint64(), bytes32(0)) + abi.encodePacked(index_.toUint64(), destinationToken_) ); bytes memory payload_ = TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_); message_ = TransceiverStructs.NttManagerMessage(bytes32(0), _alice.toBytes32(), payload_); From f3f8d9693c7373669152e6318e1f9066a15d2b39 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Sun, 1 Dec 2024 17:52:36 -0800 Subject: [PATCH 03/10] feat: add remote chain check in setRemoteSmartMToken --- src/Portal.sol | 2 ++ src/interfaces/IPortal.sol | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/Portal.sol b/src/Portal.sol index 2521a0b..bb0e329 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -80,6 +80,8 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /// @inheritdoc IPortal function setRemoteSmartMToken(uint16 remoteChainId_, bytes32 smartMToken_) external onlyOwner { + if (remoteChainId_ == chainId) revert InvalidRemoteChain(remoteChainId_); + remoteSmartMToken[remoteChainId_] = smartMToken_; emit RemoteSmartMTokenSet(remoteChainId_, smartMToken_); } diff --git a/src/interfaces/IPortal.sol b/src/interfaces/IPortal.sol index 576ec66..ade80cd 100644 --- a/src/interfaces/IPortal.sol +++ b/src/interfaces/IPortal.sol @@ -117,6 +117,10 @@ interface IPortal { /// isn't equal to EVM chainId set in the constructor. error InvalidFork(uint256 evmChainId, uint256 blockChainId); + /// @notice Emitted in `setRemoteSmartMToken` when the remote chain id + /// is equal to the local one. + error InvalidRemoteChain(uint16 remoteChainId); + /* ============ View/Pure Functions ============ */ /// @notice The current index of the Portal's earning mechanism. From a31ea0be0bd32e3fdbb20ccb2c00c4f33eef5532 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Wed, 15 Jan 2025 20:25:40 -0800 Subject: [PATCH 04/10] refactoring --- script/deploy/DeployBase.sol | 15 +-- script/upgrade/UpgradeBase.sol | 17 +-- src/HubPortal.sol | 4 +- src/Portal.sol | 125 +++++++----------- src/SpokePortal.sol | 4 +- src/governance/Migrator.sol | 15 +-- src/interfaces/IPortal.sol | 76 +++++------ test/fork/Configure.t.sol | 2 - test/fork/Migrate.t.sol | 1 - .../fixtures/migrator/MainnetMigrator.sol | 6 - .../fixtures/migrator/SepoliaMigrator.sol | 6 - test/harnesses/PortalHarness.sol | 3 +- test/unit/HubPortal.t.sol | 7 +- test/unit/Portal.t.sol | 121 ++++------------- test/unit/SpokePortal.t.sol | 7 +- 15 files changed, 118 insertions(+), 291 deletions(-) diff --git a/script/deploy/DeployBase.sol b/script/deploy/DeployBase.sol index 73c58e7..6524ceb 100644 --- a/script/deploy/DeployBase.sol +++ b/script/deploy/DeployBase.sol @@ -42,7 +42,6 @@ contract DeployBase is Script, Utils { struct HubConfiguration { address mToken; - address smartMToken; address registrar; WormholeConfiguration wormhole; } @@ -133,12 +132,7 @@ contract DeployBase is Script, Utils { } function _deployHubPortal(address deployer_, HubConfiguration memory config_) internal returns (address) { - HubPortal implementation_ = new HubPortal( - config_.mToken, - config_.smartMToken, - config_.registrar, - config_.wormhole.chainId - ); + HubPortal implementation_ = new HubPortal(config_.mToken, config_.registrar, config_.wormhole.chainId); HubPortal hubPortalProxy_ = HubPortal( _deployCreate3Proxy(address(implementation_), _computeSalt(deployer_, "Portal")) ); @@ -156,10 +150,7 @@ contract DeployBase is Script, Utils { address registrar_, uint16 wormholeChainId_ ) internal returns (address) { - // Pre-compute the expected SpokeSmartMToken proxy address. - address expectedSmartMTokenProxy_ = ContractHelper.getContractFrom(deployer_, _SPOKE_SMART_M_TOKEN_PROXY_NONCE); - - SpokePortal implementation_ = new SpokePortal(mToken_, expectedSmartMTokenProxy_, registrar_, wormholeChainId_); + SpokePortal implementation_ = new SpokePortal(mToken_, registrar_, wormholeChainId_); SpokePortal spokePortalProxy_ = SpokePortal( _deployCreate3Proxy(address(implementation_), _computeSalt(deployer_, "Portal")) ); @@ -349,11 +340,9 @@ contract DeployBase is Script, Utils { console.log("Hub configuration for chain ID %s loaded:", chainId_); hubConfig_.mToken = file_.readAddress(_readKey(hub_, "m_token")); - hubConfig_.smartMToken = file_.readAddress(_readKey(hub_, "smart_m_token")); hubConfig_.registrar = file_.readAddress(_readKey(hub_, "registrar")); console.log("M Token:", hubConfig_.mToken); - console.log("Smart M Token:", hubConfig_.smartMToken); console.log("Registrar:", hubConfig_.registrar); hubConfig_.wormhole = _loadWormholeConfig(file_, hub_); diff --git a/script/upgrade/UpgradeBase.sol b/script/upgrade/UpgradeBase.sol index a61d9bd..b7066db 100644 --- a/script/upgrade/UpgradeBase.sol +++ b/script/upgrade/UpgradeBase.sol @@ -24,7 +24,6 @@ contract UpgradeBase is Script, Utils { struct PortalConfiguration { address mToken; - address smartMToken; address registrar; address portal; uint16 wormholeChainId; @@ -58,12 +57,7 @@ contract UpgradeBase is Script, Utils { } function _upgradeHubPortal(PortalConfiguration memory config_) internal { - HubPortal implementation_ = new HubPortal( - config_.mToken, - config_.smartMToken, - config_.registrar, - config_.wormholeChainId - ); + HubPortal implementation_ = new HubPortal(config_.mToken, config_.registrar, config_.wormholeChainId); console.log("HubPortal implementation deployed at: ", address(implementation_)); @@ -71,12 +65,7 @@ contract UpgradeBase is Script, Utils { } function _upgradeSpokePortal(PortalConfiguration memory config_) internal { - SpokePortal implementation_ = new SpokePortal( - config_.mToken, - config_.smartMToken, - config_.registrar, - config_.wormholeChainId - ); + SpokePortal implementation_ = new SpokePortal(config_.mToken, config_.registrar, config_.wormholeChainId); console.log("SpokePortal implementation deployed at: ", address(implementation_)); @@ -95,13 +84,11 @@ contract UpgradeBase is Script, Utils { console.log("Portal configuration for chain ID %s loaded:", chainId_); portalConfig_.mToken = file_.readAddress(_readKey(config_, "m_token")); - portalConfig_.smartMToken = file_.readAddress(_readKey(config_, "smart_m_token")); portalConfig_.registrar = file_.readAddress(_readKey(config_, "registrar")); portalConfig_.portal = file_.readAddress(_readKey(config_, "portal")); portalConfig_.wormholeChainId = uint16(file_.readUint(_readKey(config_, "wormhole.chain_id"))); console.log("M Token:", portalConfig_.mToken); - console.log("Smart M Token:", portalConfig_.smartMToken); console.log("Registrar:", portalConfig_.registrar); console.log("Portal:", portalConfig_.portal); console.log("Wormhole chain ID:", portalConfig_.wormholeChainId); diff --git a/src/HubPortal.sol b/src/HubPortal.sol index 39925bb..87ae48f 100644 --- a/src/HubPortal.sol +++ b/src/HubPortal.sol @@ -33,16 +33,14 @@ contract HubPortal is IHubPortal, Portal { /** * @notice Constructs the contract. * @param mToken_ The address of the M token to bridge. - * @param smartMToken_ The address of the Smart M token to bridge. * @param registrar_ The address of the Registrar. * @param chainId_ Wormhole chain id. */ constructor( address mToken_, - address smartMToken_, address registrar_, uint16 chainId_ - ) Portal(mToken_, smartMToken_, registrar_, Mode.LOCKING, chainId_) {} + ) Portal(mToken_, registrar_, Mode.LOCKING, chainId_) {} /* ============ Interactive Functions ============ */ diff --git a/src/Portal.sol b/src/Portal.sol index bb0e329..d1904aa 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -37,30 +37,25 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { address public immutable registrar; /// @inheritdoc IPortal - address public immutable smartMToken; - - /// @inheritdoc IPortal - mapping(uint16 remoteChainId => bytes32 smartMToken) public remoteSmartMToken; + mapping(address sourceWrappedToken => mapping(uint16 destinationChainId => bytes32 destinationWrappedToken)) + public destinationWrappedMToken; /* ============ Constructor ============ */ /** * @notice Constructs the contract. * @param mToken_ The address of the M token to bridge. - * @param smartMToken_ The address of the Smart M token to bridge. * @param registrar_ The address of the Registrar. * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. * @param chainId_ The Wormhole chain id. */ constructor( address mToken_, - address smartMToken_, address registrar_, Mode mode_, uint16 chainId_ ) NttManagerNoRateLimiting(mToken_, mode_, chainId_) { if (mToken_ == address(0)) revert ZeroMToken(); - if ((smartMToken = smartMToken_) == address(0)) revert ZeroSmartMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); } @@ -79,47 +74,64 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /* ============ External Interactive Functions ============ */ /// @inheritdoc IPortal - function setRemoteSmartMToken(uint16 remoteChainId_, bytes32 smartMToken_) external onlyOwner { - if (remoteChainId_ == chainId) revert InvalidRemoteChain(remoteChainId_); - - remoteSmartMToken[remoteChainId_] = smartMToken_; - emit RemoteSmartMTokenSet(remoteChainId_, smartMToken_); - } - - /// @inheritdoc IPortal - function transferSmartMToken( - uint256 amount_, + function setDestinationWrappedMToken( + address sourceWrappedToken_, uint16 destinationChainId_, - bytes32 recipient_, - bytes32 refundAddress_ - ) external payable returns (bytes32 messageId_) { - messageId_ = _transferWrappedMToken( - amount_, - smartMToken, - remoteSmartMToken[destinationChainId_], - destinationChainId_, - recipient_, - refundAddress_ - ); + bytes32 destinationWrappedToken_ + ) external onlyOwner { + if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_); + + destinationWrappedMToken[sourceWrappedToken_][destinationChainId_] = destinationWrappedToken_; + emit DestinationWrappedMTokenSet(sourceWrappedToken_, destinationChainId_, destinationWrappedToken_); } /// @inheritdoc IPortal function transferWrappedMToken( uint256 amount_, address sourceWrappedToken_, - bytes32 destinationWrappedToken_, uint16 destinationChainId_, bytes32 recipient_, bytes32 refundAddress_ ) external payable returns (bytes32 messageId_) { - messageId_ = _transferWrappedMToken( - amount_, - sourceWrappedToken_, + if (amount_ == 0) revert ZeroAmount(); + if (recipient_ == bytes32(0)) revert InvalidRecipient(); + if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); + + bytes32 destinationWrappedToken_ = destinationWrappedMToken[sourceWrappedToken_][destinationChainId_]; + + if (destinationWrappedToken_ == bytes32(0)) + revert UnsupportedDestinationToken(sourceWrappedToken_, destinationChainId_); + + // transfer Wrapped M from the sender + IERC20(sourceWrappedToken_).transferFrom(msg.sender, address(this), amount_); + + // unwrap Wrapped M token to M Token + amount_ = IWrappedMTokenLike(sourceWrappedToken_).unwrap(address(this), amount_); + + // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. + // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. + + uint64 sequence_ = _useMessageSequence(); + uint128 index_ = _currentIndex(); + + TransceiverStructs.NttManagerMessage memory message_; + (, message_, messageId_) = _encodeTokenTransfer( + _trimTransferAmount(amount_, destinationChainId_), + index_, + recipient_, destinationWrappedToken_, destinationChainId_, - recipient_, - refundAddress_ + sequence_, + msg.sender ); + + uint256 totalPriceQuote_ = _sendMessage(destinationChainId_, refundAddress_, message_); + + emit MTokenSent(destinationChainId_, messageId_, msg.sender, recipient_, amount_, index_); + + // Emit NTT events + emit TransferSent(recipient_, refundAddress_, amount_, totalPriceQuote_, destinationChainId_, sequence_); + emit TransferSent(messageId_); } /* ============ Internal/Private Interactive Functions ============ */ @@ -181,51 +193,6 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_); } - /// @dev Transfers a Wrapped M token to the destination chain by unwrapping it to M Token - function _transferWrappedMToken( - uint256 amount_, - address sourceWrappedToken_, - bytes32 destinationWrappedToken_, - uint16 destinationChainId_, - bytes32 recipient_, - bytes32 refundAddress_ - ) private returns (bytes32 messageId_) { - if (amount_ == 0) revert ZeroAmount(); - if (recipient_ == bytes32(0)) revert InvalidRecipient(); - if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); - - // transfer Wrapped M from the sender - IERC20(sourceWrappedToken_).transferFrom(msg.sender, address(this), amount_); - - // unwrap Wrapped M token to M Token - amount_ = IWrappedMTokenLike(sourceWrappedToken_).unwrap(address(this), amount_); - - // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. - // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. - - uint64 sequence_ = _useMessageSequence(); - uint128 index_ = _currentIndex(); - - TransceiverStructs.NttManagerMessage memory message_; - (, message_, messageId_) = _encodeTokenTransfer( - _trimTransferAmount(amount_, destinationChainId_), - index_, - recipient_, - destinationWrappedToken_, - destinationChainId_, - sequence_, - msg.sender - ); - - uint256 totalPriceQuote_ = _sendMessage(destinationChainId_, refundAddress_, message_); - - emit MTokenSent(destinationChainId_, messageId_, msg.sender, recipient_, amount_, index_); - - // Emit NTT events - emit TransferSent(recipient_, refundAddress_, amount_, totalPriceQuote_, destinationChainId_, sequence_); - emit TransferSent(messageId_); - } - /// @notice Sends a generic message to the destination chain. /// @dev The implementation is adapted from `NttManager` `_transfer` function. function _sendMessage( diff --git a/src/SpokePortal.sol b/src/SpokePortal.sol index 8d41b6d..54de79b 100644 --- a/src/SpokePortal.sol +++ b/src/SpokePortal.sol @@ -22,16 +22,14 @@ contract SpokePortal is ISpokePortal, Portal { /** * @notice Constructs the contract. * @param mToken_ The address of the M token to bridge. - * @param smartMToken_ The address of the Smart M token to bridge. * @param registrar_ The address of the Registrar. * @param chainId_ Wormhole chain id. */ constructor( address mToken_, - address smartMToken_, address registrar_, uint16 chainId_ - ) Portal(mToken_, smartMToken_, registrar_, Mode.BURNING, chainId_) {} + ) Portal(mToken_, registrar_, Mode.BURNING, chainId_) {} /* ============ Internal/Private Interactive Functions ============ */ diff --git a/src/governance/Migrator.sol b/src/governance/Migrator.sol index 3f962ca..ad5c4d3 100644 --- a/src/governance/Migrator.sol +++ b/src/governance/Migrator.sol @@ -23,7 +23,6 @@ abstract contract Migrator is IMigrator { /// @dev Portal migration parameters. struct PortalMigrateParams { address mToken; - address smartMToken; address registrar; uint16 wormholeChainId; } @@ -67,12 +66,7 @@ abstract contract Migrator is IMigrator { * @param params_ The parameters for the migrate. */ function _migrateHubPortal(PortalMigrateParams memory params_) internal { - HubPortal implementation_ = new HubPortal( - params_.mToken, - params_.smartMToken, - params_.registrar, - params_.wormholeChainId - ); + HubPortal implementation_ = new HubPortal(params_.mToken, params_.registrar, params_.wormholeChainId); IManagerBase(portal).upgrade(address(implementation_)); } @@ -81,12 +75,7 @@ abstract contract Migrator is IMigrator { * @param params_ The parameters for the migrate. */ function _migrateSpokePortal(PortalMigrateParams memory params_) internal { - SpokePortal implementation_ = new SpokePortal( - params_.mToken, - params_.smartMToken, - params_.registrar, - params_.wormholeChainId - ); + SpokePortal implementation_ = new SpokePortal(params_.mToken, params_.registrar, params_.wormholeChainId); IManagerBase(portal).upgrade(address(implementation_)); } diff --git a/src/interfaces/IPortal.sol b/src/interfaces/IPortal.sol index ade80cd..34ce9c9 100644 --- a/src/interfaces/IPortal.sol +++ b/src/interfaces/IPortal.sol @@ -96,11 +96,16 @@ interface IPortal { event WrapFailed(address indexed destinationWrappedToken, address indexed recipient, uint256 amount); /** - * @notice Emitted when Smart M token is set for the remote chain. - * @param remoteChainId The Wormhole remote chain ID. - * @param smartMToken The address of the Smart M Token on the remote chain. + * @notice Emitted when Wrapped M token is set for the remote chain. + * @param sourceWrappedToken The address of Wrapped M token on the source chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param destinationWrappedToken The address of Wrapped M token on the destination chain. */ - event RemoteSmartMTokenSet(uint16 remoteChainId, bytes32 smartMToken); + event DestinationWrappedMTokenSet( + address indexed sourceWrappedToken, + uint16 indexed destinationChainId, + bytes32 destinationWrappedToken + ); /* ============ Custom Errors ============ */ @@ -117,9 +122,13 @@ interface IPortal { /// isn't equal to EVM chainId set in the constructor. error InvalidFork(uint256 evmChainId, uint256 blockChainId); - /// @notice Emitted in `setRemoteSmartMToken` when the remote chain id - /// is equal to the local one. - error InvalidRemoteChain(uint16 remoteChainId); + /// @notice Emitted in `setDestinationWrappedMToken` function when the destination chain id + /// is equal to the source one. + error InvalidDestinationChain(uint16 destinationChainId); + + /// @notice Emitted in `transferWrappedMToken` function when the destination Wrapped token is not registered + /// for the given `sourceWrappedToken` and `destinationChainId`. + error UnsupportedDestinationToken(address sourceWrappedToken, uint16 destinationChainId); /* ============ View/Pure Functions ============ */ @@ -132,52 +141,43 @@ interface IPortal { /// @notice The address of the Registrar contract. function registrar() external view returns (address); - /// @notice The address of the Smart M token. - function smartMToken() external view returns (address); - /** - * @notice Returns the address of the Smart M Token on the remote chain. - * @param remoteChainId The Wormhole remote chain ID. - * @return smartMToken address on the remote chain. + * @notice Returns the address of the Wrapped M token on the remote chain. + * @param sourceWrappedToken The address of Wrapped M token on the source chain. + * @param destinationChainId The Wormhole destination chain ID. + * @return destinationWrappedToken The address of Wrapped M token on the destination chain. */ - function remoteSmartMToken(uint16 remoteChainId) external view returns (bytes32 smartMToken); + function destinationWrappedMToken( + address sourceWrappedToken, + uint16 destinationChainId + ) external view returns (bytes32 destinationWrappedToken); /* ============ Interactive Functions ============ */ - /// @notice Sets the address of Smart M Token on the remote chain. - function setRemoteSmartMToken(uint16 remoteChainId, bytes32 smartMToken) external; - /** - * @notice Transfers Smart M Token to the destination chain. - * @param amount The amount of tokens to transfer. - * @param destinationChainId The Wormhole destination chain ID. - * @param recipient The account to receive tokens. - * @param refundAddress The address to receive excess native gas on the destination chain. - * @return messageId The ID uniquely identifying the message. + * @notice Sets the address of Wrapped M Token on the remote chain. + * @param sourceWrappedToken The address of Wrapped M token on the source chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param destinationWrappedToken The address of Wrapped M token on the destination chain. */ - function transferSmartMToken( - uint256 amount, + function setDestinationWrappedMToken( + address sourceWrappedToken, uint16 destinationChainId, - bytes32 recipient, - bytes32 refundAddress - ) external payable returns (bytes32 messageId); + bytes32 destinationWrappedToken + ) external; /** * @notice Transfers Wrapped M Token to the destination chain. - * @dev Can be used for transferring M Token Extensions and converting between different Wrappers. - * @param amount The amount of tokens to transfer. - * @param sourceWrappedToken The address of the Wrapped M Token of the source chain. - * @param destinationWrappedToken The address of the Wrapped M Token of the destination chain. - * @param amount The amount of tokens to transfer. - * @param destinationChainId The Wormhole destination chain ID. - * @param recipient The account to receive tokens. - * @param refundAddress The address to receive excess native gas on the destination chain. - * @return messageId The ID uniquely identifying the message. + * @param amount The amount of tokens to transfer. + * @param sourceWrappedToken The address of the Wrapped M Token of the source chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param recipient The account to receive tokens. + * @param refundAddress The address to receive excess native gas on the destination chain. + * @return messageId The ID uniquely identifying the message. */ function transferWrappedMToken( uint256 amount, address sourceWrappedToken, - bytes32 destinationWrappedToken, uint16 destinationChainId, bytes32 recipient, bytes32 refundAddress diff --git a/test/fork/Configure.t.sol b/test/fork/Configure.t.sol index e3c2d70..3f119e9 100644 --- a/test/fork/Configure.t.sol +++ b/test/fork/Configure.t.sol @@ -42,7 +42,6 @@ contract Configure is ForkTestBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, - _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); @@ -137,7 +136,6 @@ contract Configure is ForkTestBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, - _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); diff --git a/test/fork/Migrate.t.sol b/test/fork/Migrate.t.sol index 9625ca0..f1cdf9f 100644 --- a/test/fork/Migrate.t.sol +++ b/test/fork/Migrate.t.sol @@ -60,7 +60,6 @@ contract Migrate is ForkTestBase, UpgradeBase { HubPortal hubPortalImplementation_ = new HubPortal( _MAINNET_M_TOKEN, - _MAINNET_SMART_M_TOKEN, _MAINNET_REGISTRAR, _MAINNET_WORMHOLE_CHAIN_ID ); diff --git a/test/fork/fixtures/migrator/MainnetMigrator.sol b/test/fork/fixtures/migrator/MainnetMigrator.sol index a855798..6fe6ed5 100644 --- a/test/fork/fixtures/migrator/MainnetMigrator.sol +++ b/test/fork/fixtures/migrator/MainnetMigrator.sol @@ -22,9 +22,6 @@ contract MainnetMigrator is Migrator { /// @dev Mainnet MToken address. address internal constant _MAINNET_M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; - /// @dev Mainnet Smart MToken address. - address internal constant _MAINNET_SMART_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; - /// @dev Mainnet Registrar address. address internal constant _MAINNET_REGISTRAR = 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A; @@ -52,7 +49,6 @@ contract MainnetMigrator is Migrator { _migrateHubPortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, - smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _MAINNET_WORMHOLE_CHAIN_ID }) @@ -72,7 +68,6 @@ contract MainnetMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, - smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _BASE_WORMHOLE_CHAIN_ID }) @@ -92,7 +87,6 @@ contract MainnetMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _MAINNET_M_TOKEN, - smartMToken: _MAINNET_SMART_M_TOKEN, registrar: _MAINNET_REGISTRAR, wormholeChainId: _OPTIMISM_WORMHOLE_CHAIN_ID }) diff --git a/test/fork/fixtures/migrator/SepoliaMigrator.sol b/test/fork/fixtures/migrator/SepoliaMigrator.sol index fa65e81..62a14b6 100644 --- a/test/fork/fixtures/migrator/SepoliaMigrator.sol +++ b/test/fork/fixtures/migrator/SepoliaMigrator.sol @@ -22,9 +22,6 @@ contract SepoliaMigrator is Migrator { /// @dev Sepolia Spoke M token address. address internal constant _SEPOLIA_SPOKE_M_TOKEN = 0xCEC6566b227a95C76a0E3dbFdC7794CA795C7F9e; - /// @dev Sepolia Spoke Smart M token address. - address internal constant _SEPOLIA_SPOKE_SMART_M_TOKEN = 0xCEC6566b227a95C76a0E3dbFdC7794CA795C7F9e; - /// @dev Sepolia Spoke Registrar address. address internal constant _SEPOLIA_SPOKE_REGISTRAR = 0x39a5F8C5ADC500E1d30115c09A1016764D90bC94; @@ -52,7 +49,6 @@ contract SepoliaMigrator is Migrator { _migrateHubPortal( PortalMigrateParams({ mToken: 0x0c941AD94Ca4A52EDAeAbF203b61bdd1807CeEC0, - smartMToken: 0x437cc33344a0B27A429f795ff6B469C72698B291, registrar: 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A, wormholeChainId: _SEPOLIA_WORMHOLE_CHAIN_ID }) @@ -72,7 +68,6 @@ contract SepoliaMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _SEPOLIA_SPOKE_M_TOKEN, - smartMToken: _SEPOLIA_SPOKE_SMART_M_TOKEN, registrar: _SEPOLIA_SPOKE_REGISTRAR, wormholeChainId: _BASE_SEPOLIA_WORMHOLE_CHAIN_ID }) @@ -92,7 +87,6 @@ contract SepoliaMigrator is Migrator { _migrateSpokePortal( PortalMigrateParams({ mToken: _SEPOLIA_SPOKE_M_TOKEN, - smartMToken: _SEPOLIA_SPOKE_SMART_M_TOKEN, registrar: _SEPOLIA_SPOKE_REGISTRAR, wormholeChainId: _OPTIMISM_SEPOLIA_WORMHOLE_CHAIN_ID }) diff --git a/test/harnesses/PortalHarness.sol b/test/harnesses/PortalHarness.sol index 9d6da2f..fd87449 100644 --- a/test/harnesses/PortalHarness.sol +++ b/test/harnesses/PortalHarness.sol @@ -7,9 +7,8 @@ import { Portal } from "../../src/Portal.sol"; contract PortalHarness is Portal { constructor( address mToken_, - address smartMToken_, address registrar_, Mode mode_, uint16 chainId_ - ) Portal(mToken_, smartMToken_, registrar_, mode_, chainId_) {} + ) Portal(mToken_, registrar_, mode_, chainId_) {} } diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index ffdda56..1f2b969 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -37,12 +37,7 @@ contract HubPortalTests is UnitTestBase { _registrar = new MockHubRegistrar(); _transceiver = new MockTransceiver(); - HubPortal implementation_ = new HubPortal( - address(_mToken), - address(_smartMToken), - address(_registrar), - _LOCAL_CHAIN_ID - ); + HubPortal implementation_ = new HubPortal(address(_mToken), address(_registrar), _LOCAL_CHAIN_ID); _portal = HubPortal(_createProxy(address(implementation_))); _initializePortal(_portal); diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index da5f50d..7ae202a 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -23,14 +23,14 @@ contract PortalTests is UnitTestBase { using TrimmedAmountLib for *; MockSpokeMToken internal _mToken; - MockWrappedMToken internal _smartMToken; + MockWrappedMToken internal _wrappedMToken; MockSpokeRegistrar internal _registrar; PortalHarness internal _portal; function setUp() external { _mToken = new MockSpokeMToken(); - _smartMToken = new MockWrappedMToken(address(_mToken)); + _wrappedMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -40,7 +40,6 @@ contract PortalTests is UnitTestBase { PortalHarness implementation_ = new PortalHarness( address(_mToken), - address(_smartMToken), address(_registrar), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID @@ -53,24 +52,12 @@ contract PortalTests is UnitTestBase { function test_constructor_zeroMToken() external { vm.expectRevert(IPortal.ZeroMToken.selector); - new PortalHarness( - address(0), - address(_smartMToken), - address(_registrar), - IManagerBase.Mode.BURNING, - _LOCAL_CHAIN_ID - ); + new PortalHarness(address(0), address(_registrar), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID); } function test_constructor_zeroRegistrar() external { vm.expectRevert(IPortal.ZeroRegistrar.selector); - new PortalHarness( - address(_mToken), - address(_smartMToken), - address(0), - IManagerBase.Mode.BURNING, - _LOCAL_CHAIN_ID - ); + new PortalHarness(address(_mToken), address(0), IManagerBase.Mode.BURNING, _LOCAL_CHAIN_ID); } /* ============ transfer ============ */ @@ -146,133 +133,71 @@ contract PortalTests is UnitTestBase { function test_transferWrappedMToken_zeroAmount() external { uint256 amount_ = 0; - bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); bytes32 recipient_ = _alice.toBytes32(); bytes32 refundAddress_ = recipient_; vm.expectRevert(INttManager.ZeroAmount.selector); - _portal.transferWrappedMToken( - amount_, - address(_smartMToken), - destinationWrappedToken_, - _REMOTE_CHAIN_ID, - recipient_, - refundAddress_ - ); + _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); } function test_transferWrappedMToken_zeroRecipient() external { uint256 amount_ = 1_000e6; - bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); bytes32 recipient_ = bytes32(0); bytes32 refundAddress_ = _alice.toBytes32(); vm.expectRevert(INttManager.InvalidRecipient.selector); - _portal.transferWrappedMToken( - amount_, - address(_smartMToken), - destinationWrappedToken_, - _REMOTE_CHAIN_ID, - recipient_, - refundAddress_ - ); + _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); } function test_transferWrappedMToken_zeroRefundAddress() external { uint256 amount_ = 1_000e6; - bytes32 destinationWrappedToken_ = address(_smartMToken).toBytes32(); bytes32 recipient_ = _alice.toBytes32(); bytes32 refundAddress_ = bytes32(0); vm.expectRevert(INttManager.InvalidRefundAddress.selector); - _portal.transferWrappedMToken( - amount_, - address(_smartMToken), - destinationWrappedToken_, - _REMOTE_CHAIN_ID, - recipient_, - refundAddress_ - ); + _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); } - function test_transferWrappedMToken() external { + function test_transferWrappedMToken_unsupportedDestinationToken() external { uint256 amount_ = 1_000e6; - uint128 index_ = 0; - bytes32 destinationWrappedToken_ = makeAddr("wrapped M").toBytes32(); bytes32 recipient_ = _alice.toBytes32(); bytes32 refundAddress_ = recipient_; - (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( - amount_, - index_, - recipient_, - _LOCAL_CHAIN_ID, - _REMOTE_CHAIN_ID, - destinationWrappedToken_ - ); - - _mToken.mint(_alice, amount_); - - vm.startPrank(_alice); - _mToken.approve(address(_smartMToken), amount_); - amount_ = _smartMToken.wrap(_alice, amount_); - _smartMToken.approve(address(_portal), amount_); - - // expect to call sendMessage in Transceiver - vm.expectCall( - address(_transceiver), - 0, - abi.encodeCall( - _transceiver.sendMessage, - ( - _REMOTE_CHAIN_ID, - _emptyTransceiverInstruction, - TransceiverStructs.encodeNttManagerMessage(message_), - _PEER, - recipient_ - ) + vm.expectRevert( + abi.encodeWithSelector( + IPortal.UnsupportedDestinationToken.selector, + address(_wrappedMToken), + _REMOTE_CHAIN_ID ) ); - vm.expectEmit(); - emit IPortal.MTokenSent(_REMOTE_CHAIN_ID, messageId_, _alice, recipient_, amount_, index_); - - vm.expectEmit(); - emit INttManager.TransferSent(messageId_); - - _portal.transferWrappedMToken( - amount_, - address(_smartMToken), - destinationWrappedToken_, - _REMOTE_CHAIN_ID, - recipient_, - refundAddress_ - ); + _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); } - function test_transferSmartMToken() external { + function test_transferWrappedMToken() external { uint256 amount_ = 1_000e6; uint128 index_ = 0; - bytes32 destinationSmartMToken_ = makeAddr("smart M").toBytes32(); + bytes32 destinationWrappedToken_ = makeAddr("wrapped M").toBytes32(); bytes32 recipient_ = _alice.toBytes32(); bytes32 refundAddress_ = recipient_; + _portal.setDestinationWrappedMToken(address(_wrappedMToken), _REMOTE_CHAIN_ID, destinationWrappedToken_); + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( amount_, index_, recipient_, _LOCAL_CHAIN_ID, _REMOTE_CHAIN_ID, - destinationSmartMToken_ + destinationWrappedToken_ ); _mToken.mint(_alice, amount_); - _portal.setRemoteSmartMToken(_REMOTE_CHAIN_ID, destinationSmartMToken_); vm.startPrank(_alice); - _mToken.approve(address(_smartMToken), amount_); - amount_ = _smartMToken.wrap(_alice, amount_); - _smartMToken.approve(address(_portal), amount_); + _mToken.approve(address(_wrappedMToken), amount_); + amount_ = _wrappedMToken.wrap(_alice, amount_); + _wrappedMToken.approve(address(_portal), amount_); // expect to call sendMessage in Transceiver vm.expectCall( @@ -296,7 +221,7 @@ contract PortalTests is UnitTestBase { vm.expectEmit(); emit INttManager.TransferSent(messageId_); - _portal.transferSmartMToken(amount_, _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); } /* ============ _handleMsg ============ */ diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index 6d61f88..a5cd54f 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -37,12 +37,7 @@ contract SpokePortalTests is UnitTestBase { _registrar = new MockSpokeRegistrar(); _transceiver = new MockTransceiver(); - SpokePortal implementation_ = new SpokePortal( - address(_mToken), - address(_smartMToken), - address(_registrar), - _LOCAL_CHAIN_ID - ); + SpokePortal implementation_ = new SpokePortal(address(_mToken), address(_registrar), _LOCAL_CHAIN_ID); _portal = SpokePortal(_createProxy(address(implementation_))); _initializePortal(_portal); From 6e716a385f122a1b9a1a3c68cb4594d69db61f6c Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Wed, 15 Jan 2025 20:37:48 -0800 Subject: [PATCH 05/10] cleanup --- script/helpers/Utils.sol | 1 - src/HubPortal.sol | 6 +++--- src/Portal.sol | 8 ++++---- src/SpokePortal.sol | 6 +++--- test/fork/fixtures/deploy-config.json | 1 - test/fork/fixtures/upgrade-config.json | 1 - test/unit/HubPortal.t.sol | 2 -- test/unit/SpokePortal.t.sol | 8 ++++---- 8 files changed, 14 insertions(+), 19 deletions(-) diff --git a/script/helpers/Utils.sol b/script/helpers/Utils.sol index c323dc8..aad70f2 100644 --- a/script/helpers/Utils.sol +++ b/script/helpers/Utils.sol @@ -20,7 +20,6 @@ contract Utils { address internal constant _MAINNET_REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _MAINNET_M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; - address internal constant _MAINNET_SMART_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; address internal constant _MAINNET_VAULT = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; address internal constant _SEPOLIA_REGISTRAR = 0x975Bf5f212367D09CB7f69D3dc4BA8C9B440aD3A; diff --git a/src/HubPortal.sol b/src/HubPortal.sol index 87ae48f..0d1384c 100644 --- a/src/HubPortal.sol +++ b/src/HubPortal.sol @@ -32,9 +32,9 @@ contract HubPortal is IHubPortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, diff --git a/src/Portal.sol b/src/Portal.sol index d1904aa..a2a41b4 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -44,10 +44,10 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. - * @param chainId_ The Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. + * @param chainId_ The Wormhole chain id. */ constructor( address mToken_, diff --git a/src/SpokePortal.sol b/src/SpokePortal.sol index 54de79b..0994b18 100644 --- a/src/SpokePortal.sol +++ b/src/SpokePortal.sol @@ -21,9 +21,9 @@ contract SpokePortal is ISpokePortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, diff --git a/test/fork/fixtures/deploy-config.json b/test/fork/fixtures/deploy-config.json index ac5ff4a..c2cb506 100644 --- a/test/fork/fixtures/deploy-config.json +++ b/test/fork/fixtures/deploy-config.json @@ -2,7 +2,6 @@ "hub": { "1": { "m_token": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", - "smart_m_token": "0x437cc33344a0B27A429f795ff6B469C72698B291", "registrar": "0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c", "wormhole": { "chain_id": "2", diff --git a/test/fork/fixtures/upgrade-config.json b/test/fork/fixtures/upgrade-config.json index 3194a90..d37c296 100644 --- a/test/fork/fixtures/upgrade-config.json +++ b/test/fork/fixtures/upgrade-config.json @@ -2,7 +2,6 @@ "config": { "1": { "m_token": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", - "smart_m_token": "0x437cc33344a0B27A429f795ff6B469C72698B291", "registrar": "0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c", "portal": "0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd", "wormhole": { diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index 1f2b969..8aa7cf8 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -22,14 +22,12 @@ contract HubPortalTests is UnitTestBase { using TypeConverter for *; MockHubMToken internal _mToken; - MockWrappedMToken internal _smartMToken; MockHubRegistrar internal _registrar; HubPortal internal _portal; function setUp() external { _mToken = new MockHubMToken(); - _smartMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index a5cd54f..a40efb3 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -22,14 +22,14 @@ contract SpokePortalTests is UnitTestBase { using TypeConverter for *; MockSpokeMToken internal _mToken; - MockWrappedMToken internal _smartMToken; + MockWrappedMToken internal _wrappedMToken; MockSpokeRegistrar internal _registrar; SpokePortal internal _portal; function setUp() external { _mToken = new MockSpokeMToken(); - _smartMToken = new MockWrappedMToken(address(_mToken)); + _wrappedMToken = new MockWrappedMToken(address(_mToken)); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -332,11 +332,11 @@ contract SpokePortalTests is UnitTestBase { _alice.toBytes32(), _REMOTE_CHAIN_ID, _LOCAL_CHAIN_ID, - address(_smartMToken).toBytes32() + address(_wrappedMToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", address(_portal), amount_)); - vm.expectCall(address(_smartMToken), abi.encodeWithSignature("wrap(address,uint256)", _alice, amount_)); + vm.expectCall(address(_wrappedMToken), abi.encodeWithSignature("wrap(address,uint256)", _alice, amount_)); vm.expectEmit(); emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); From c1bad5deea9ff66c85bbc90a44690ab869c775a3 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Wed, 15 Jan 2025 20:40:54 -0800 Subject: [PATCH 06/10] formatting --- src/HubPortal.sol | 6 +++--- src/Portal.sol | 8 ++++---- src/SpokePortal.sol | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/HubPortal.sol b/src/HubPortal.sol index 0d1384c..06cf7e2 100644 --- a/src/HubPortal.sol +++ b/src/HubPortal.sol @@ -32,9 +32,9 @@ contract HubPortal is IHubPortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, diff --git a/src/Portal.sol b/src/Portal.sol index a2a41b4..a1b84b9 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -44,10 +44,10 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. - * @param chainId_ The Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param mode_ The NttManager token transfer mode - LOCKING or BURNING. + * @param chainId_ The Wormhole chain id. */ constructor( address mToken_, diff --git a/src/SpokePortal.sol b/src/SpokePortal.sol index 0994b18..2aa2b0c 100644 --- a/src/SpokePortal.sol +++ b/src/SpokePortal.sol @@ -21,9 +21,9 @@ contract SpokePortal is ISpokePortal, Portal { /** * @notice Constructs the contract. - * @param mToken_ The address of the M token to bridge. - * @param registrar_ The address of the Registrar. - * @param chainId_ Wormhole chain id. + * @param mToken_ The address of the M token to bridge. + * @param registrar_ The address of the Registrar. + * @param chainId_ Wormhole chain id. */ constructor( address mToken_, From bd66230eaa43ccba275311230ea8cbd1746558bc Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Sun, 19 Jan 2025 22:41:11 -0800 Subject: [PATCH 07/10] feat: allow M to wrapped M bridging --- src/Portal.sol | 84 ++++++++++++++++-------- src/interfaces/IPortal.sol | 120 +++++++++++++++++------------------ test/unit/HubPortal.t.sol | 26 +++++--- test/unit/Portal.t.sol | 80 +++++++++++++++++------ test/unit/SpokePortal.t.sol | 57 ++++++++++++----- test/unit/UnitTestBase.t.sol | 17 ----- 6 files changed, 236 insertions(+), 148 deletions(-) diff --git a/src/Portal.sol b/src/Portal.sol index a1b84b9..7658497 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -31,14 +31,12 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /// @dev Use only standard WormholeTransceiver with relaying enabled bytes public constant DEFAULT_TRANSCEIVER_INSTRUCTIONS = new bytes(1); - bytes32 constant EMPTY_WRAPPER_ADDRESS = bytes32(0); - /// @inheritdoc IPortal address public immutable registrar; /// @inheritdoc IPortal - mapping(address sourceWrappedToken => mapping(uint16 destinationChainId => bytes32 destinationWrappedToken)) - public destinationWrappedMToken; + mapping(uint16 destinationChainId => mapping(bytes32 destinationToken => bool supported)) + public supportedDestinationToken; /* ============ Constructor ============ */ @@ -74,39 +72,42 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /* ============ External Interactive Functions ============ */ /// @inheritdoc IPortal - function setDestinationWrappedMToken( - address sourceWrappedToken_, + function setSupportedDestinationToken( uint16 destinationChainId_, - bytes32 destinationWrappedToken_ + bytes32 destinationToken_, + bool supported_ ) external onlyOwner { if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_); + if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken(); - destinationWrappedMToken[sourceWrappedToken_][destinationChainId_] = destinationWrappedToken_; - emit DestinationWrappedMTokenSet(sourceWrappedToken_, destinationChainId_, destinationWrappedToken_); + supportedDestinationToken[destinationChainId_][destinationToken_] = supported_; + emit SupportedDestinationTokenSet(destinationChainId_, destinationToken_, supported_); } /// @inheritdoc IPortal function transferWrappedMToken( uint256 amount_, - address sourceWrappedToken_, + address sourceToken_, + bytes32 destinationToken_, uint16 destinationChainId_, bytes32 recipient_, bytes32 refundAddress_ ) external payable returns (bytes32 messageId_) { if (amount_ == 0) revert ZeroAmount(); + if (sourceToken_ == address(0)) revert ZeroSourceToken(); + if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken(); if (recipient_ == bytes32(0)) revert InvalidRecipient(); if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); + if (!supportedDestinationToken[destinationChainId_][destinationToken_]) + revert UnsupportedDestinationToken(destinationChainId_, destinationToken_); - bytes32 destinationWrappedToken_ = destinationWrappedMToken[sourceWrappedToken_][destinationChainId_]; - - if (destinationWrappedToken_ == bytes32(0)) - revert UnsupportedDestinationToken(sourceWrappedToken_, destinationChainId_); + // transfer source token from the sender + IERC20(sourceToken_).transferFrom(msg.sender, address(this), amount_); - // transfer Wrapped M from the sender - IERC20(sourceWrappedToken_).transferFrom(msg.sender, address(this), amount_); - - // unwrap Wrapped M token to M Token - amount_ = IWrappedMTokenLike(sourceWrappedToken_).unwrap(address(this), amount_); + // if the source token isn't M token, unwrap it + if (sourceToken_ != token) { + amount_ = IWrappedMTokenLike(sourceToken_).unwrap(address(this), amount_); + } // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. @@ -119,7 +120,7 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { _trimTransferAmount(amount_, destinationChainId_), index_, recipient_, - destinationWrappedToken_, + destinationToken_, destinationChainId_, sequence_, msg.sender @@ -127,10 +128,29 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { uint256 totalPriceQuote_ = _sendMessage(destinationChainId_, refundAddress_, message_); - emit MTokenSent(destinationChainId_, messageId_, msg.sender, recipient_, amount_, index_); + // Stack too deep + uint256 transferAmount_ = amount_; + + emit WrappedMTokenSent( + destinationChainId_, + sourceToken_, + destinationToken_, + messageId_, + msg.sender, + recipient_, + transferAmount_, + index_ + ); // Emit NTT events - emit TransferSent(recipient_, refundAddress_, amount_, totalPriceQuote_, destinationChainId_, sequence_); + emit TransferSent( + recipient_, + refundAddress_, + transferAmount_, + totalPriceQuote_, + destinationChainId_, + sequence_ + ); emit TransferSent(messageId_); } /* ============ Internal/Private Interactive Functions ============ */ @@ -151,7 +171,7 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { amount_, index_, recipient_, - EMPTY_WRAPPER_ADDRESS, + token.toBytes32(), destinationChainId_, sequence_, sender_ @@ -247,7 +267,7 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { ( TrimmedAmount trimmedAmount_, uint128 index_, - address destinationWrappedToken_, + address destinationToken_, address recipient_, uint16 destinationChainId_ ) = payload_.decodeTokenTransfer(); @@ -257,20 +277,28 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { // NOTE: Assumes that token.decimals() are the same on all chains. uint256 amount_ = trimmedAmount_.untrim(tokenDecimals()); - emit MTokenReceived(sourceChainId_, messageId_, sender_, recipient_, amount_, index_); - // Emitting `INttManager.TransferRedeemed` to comply with Wormhole NTT specification. emit TransferRedeemed(messageId_); - if (destinationWrappedToken_ == address(0)) { + if (destinationToken_ == token) { + emit MTokenReceived(sourceChainId_, messageId_, sender_, recipient_, amount_, index_); // mints or unlocks M Token to the recipient _mintOrUnlock(recipient_, amount_, index_); } else { + emit WrappedMTokenReceived( + sourceChainId_, + destinationToken_, + messageId_, + sender_, + recipient_, + amount_, + index_ + ); // mints or unlocks M Token to the Portal _mintOrUnlock(address(this), amount_, index_); // wraps M token and transfers it to the recipient - _wrap(destinationWrappedToken_, recipient_, amount_); + _wrap(destinationToken_, recipient_, amount_); } } diff --git a/src/interfaces/IPortal.sol b/src/interfaces/IPortal.sol index 34ce9c9..9244911 100644 --- a/src/interfaces/IPortal.sol +++ b/src/interfaces/IPortal.sol @@ -16,7 +16,7 @@ interface IPortal { * @param sender The address that bridged the M tokens via the Portal. * @param recipient The account receiving tokens on destination chain. * @param amount The amount of tokens. - * @param index The the M token index. + * @param index The M token index. */ event MTokenSent( uint16 indexed destinationChainId, @@ -29,19 +29,19 @@ interface IPortal { /** * @notice Emitted when Wrapped M token is sent to a destination chain. - * @param destinationChainId The Wormhole destination chain ID. - * @param sourceWrappedToken The address of Wrapped M Token on the source chain. - * @param destinationWrappedToken The address of Wrapped M Token on the destination chain. - * @param messageId The unique identifier for the sent message. - * @param sender The address that bridged the M tokens via the Portal. - * @param recipient The account receiving tokens on destination chain. - * @param amount The amount of tokens. - * @param index The the M token index. + * @param destinationChainId The Wormhole destination chain ID. + * @param sourceToken The address of the token on the source chain. + * @param destinationToken The address of the token on the destination chain. + * @param messageId The unique identifier for the sent message. + * @param sender The address that bridged the M tokens via the Portal. + * @param recipient The account receiving tokens on destination chain. + * @param amount The amount of tokens. + * @param index The M token index. */ event WrappedMTokenSent( uint16 destinationChainId, - address indexed sourceWrappedToken, - bytes32 destinationWrappedToken, + address indexed sourceToken, + bytes32 destinationToken, bytes32 messageId, address indexed sender, bytes32 indexed recipient, @@ -56,7 +56,7 @@ interface IPortal { * @param sender The account sending tokens. * @param recipient The account receiving tokens. * @param amount The amount of tokens. - * @param index The the M token index. + * @param index The M token index. */ event MTokenReceived( uint16 indexed sourceChainId, @@ -69,17 +69,17 @@ interface IPortal { /** * @notice Emitted when Wrapped M token is received from a source chain. - * @param sourceChainId The Wormhole source chain ID. - * @param destinationWrappedToken The address of the Wrapped M Token on the destination chain. - * @param messageId The unique identifier for the received message. - * @param sender The account sending tokens. - * @param recipient The account receiving tokens. - * @param amount The amount of tokens. - * @param index The the M token index. + * @param sourceChainId The Wormhole source chain ID. + * @param destinationToken The address of the token on the destination chain. + * @param messageId The unique identifier for the received message. + * @param sender The account sending tokens. + * @param recipient The account receiving tokens. + * @param amount The amount of tokens. + * @param index The M token index. */ event WrappedMTokenReceived( uint16 sourceChainId, - address indexed destinationWrappedToken, + address indexed destinationToken, bytes32 messageId, bytes32 indexed sender, address indexed recipient, @@ -89,22 +89,22 @@ interface IPortal { /** * @notice Emitted when wrapping M token is failed on the destination. - * @param destinationWrappedToken The address of the Wrapped M Token on the destination chain. - * @param recipient The account receiving tokens. - * @param amount The amount of tokens. + * @param destinationWrappedToken The address of the Wrapped M Token on the destination chain. + * @param recipient The account receiving tokens. + * @param amount The amount of tokens. */ event WrapFailed(address indexed destinationWrappedToken, address indexed recipient, uint256 amount); /** - * @notice Emitted when Wrapped M token is set for the remote chain. - * @param sourceWrappedToken The address of Wrapped M token on the source chain. - * @param destinationChainId The Wormhole destination chain ID. - * @param destinationWrappedToken The address of Wrapped M token on the destination chain. + * @notice Emitted when a supported token is set for the remote chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param destinationToken The address of the token on the destination chain. + * @param supported `True` if the token is supported, `false` otherwise. */ - event DestinationWrappedMTokenSet( - address indexed sourceWrappedToken, + event SupportedDestinationTokenSet( uint16 indexed destinationChainId, - bytes32 destinationWrappedToken + bytes32 indexed destinationToken, + bool supported ); /* ============ Custom Errors ============ */ @@ -112,12 +112,15 @@ interface IPortal { /// @notice Emitted when the M token is 0x0. error ZeroMToken(); - /// @notice Emitted when the Smart M token is 0x0. - error ZeroSmartMToken(); - /// @notice Emitted when the Registrar address is 0x0. error ZeroRegistrar(); + /// @notice Emitted when the source token address is 0x0. + error ZeroSourceToken(); + + /// @notice Emitted when the destination token address is 0x0. + error ZeroDestinationToken(); + /// @notice Emitted when a message received if the block.chainId /// isn't equal to EVM chainId set in the constructor. error InvalidFork(uint256 evmChainId, uint256 blockChainId); @@ -126,9 +129,8 @@ interface IPortal { /// is equal to the source one. error InvalidDestinationChain(uint16 destinationChainId); - /// @notice Emitted in `transferWrappedMToken` function when the destination Wrapped token is not registered - /// for the given `sourceWrappedToken` and `destinationChainId`. - error UnsupportedDestinationToken(address sourceWrappedToken, uint16 destinationChainId); + /// @notice Emitted in `transferWrappedMToken` function when the token is not supported on the destination chain. + error UnsupportedDestinationToken(uint16 destinationChainId, bytes32 destinationToken); /* ============ View/Pure Functions ============ */ @@ -142,42 +144,40 @@ interface IPortal { function registrar() external view returns (address); /** - * @notice Returns the address of the Wrapped M token on the remote chain. - * @param sourceWrappedToken The address of Wrapped M token on the source chain. - * @param destinationChainId The Wormhole destination chain ID. - * @return destinationWrappedToken The address of Wrapped M token on the destination chain. + * @notice Indicates whether the provided token is supported on the destination chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param destinationToken The address of the token on the destination chain. + * @return supported `True` if the token is supported, `false` otherwise. */ - function destinationWrappedMToken( - address sourceWrappedToken, - uint16 destinationChainId - ) external view returns (bytes32 destinationWrappedToken); + function supportedDestinationToken( + uint16 destinationChainId, + bytes32 destinationToken + ) external view returns (bool supported); /* ============ Interactive Functions ============ */ /** - * @notice Sets the address of Wrapped M Token on the remote chain. - * @param sourceWrappedToken The address of Wrapped M token on the source chain. - * @param destinationChainId The Wormhole destination chain ID. - * @param destinationWrappedToken The address of Wrapped M token on the destination chain. + * @notice Sets whether the token is supported on the remote chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param destinationToken The address of the token on the destination chain. + * @param supported `True` if the token is supported, `false` otherwise. */ - function setDestinationWrappedMToken( - address sourceWrappedToken, - uint16 destinationChainId, - bytes32 destinationWrappedToken - ) external; + function setSupportedDestinationToken(uint16 destinationChainId, bytes32 destinationToken, bool supported) external; /** * @notice Transfers Wrapped M Token to the destination chain. - * @param amount The amount of tokens to transfer. - * @param sourceWrappedToken The address of the Wrapped M Token of the source chain. - * @param destinationChainId The Wormhole destination chain ID. - * @param recipient The account to receive tokens. - * @param refundAddress The address to receive excess native gas on the destination chain. - * @return messageId The ID uniquely identifying the message. + * @param amount The amount of tokens to transfer. + * @param sourceToken The address of the token (M or Wrapped M) on the source chain. + * @param destinationToken The address of the token (M or Wrapped M) on the destination chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param recipient The account to receive tokens. + * @param refundAddress The address to receive excess native gas on the destination chain. + * @return messageId The ID uniquely identifying the message. */ function transferWrappedMToken( uint256 amount, - address sourceWrappedToken, + address sourceToken, + bytes32 destinationToken, uint16 destinationChainId, bytes32 recipient, bytes32 refundAddress diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index 8aa7cf8..6836f78 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -310,16 +310,19 @@ contract HubPortalTests is UnitTestBase { /* ============ receiveMToken ============ */ function test_receiveMToken_invalidTargetChain() external { + uint16 invalidChainId = 1111; + (TransceiverStructs.NttManagerMessage memory message_, ) = _createTransferMessage( 1_000e6, _EXP_SCALED_ONE, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _REMOTE_CHAIN_ID + invalidChainId, + address(_mToken).toBytes32() ); vm.expectRevert( - abi.encodeWithSelector(INttManager.InvalidTargetChain.selector, _REMOTE_CHAIN_ID, _LOCAL_CHAIN_ID) + abi.encodeWithSelector(INttManager.InvalidTargetChain.selector, invalidChainId, _LOCAL_CHAIN_ID) ); vm.prank(address(_transceiver)); @@ -337,16 +340,17 @@ contract HubPortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); @@ -366,7 +370,8 @@ contract HubPortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); @@ -390,7 +395,8 @@ contract HubPortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); @@ -414,7 +420,8 @@ contract HubPortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); @@ -439,7 +446,8 @@ contract HubPortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index 7ae202a..7837d16 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -25,12 +25,14 @@ contract PortalTests is UnitTestBase { MockSpokeMToken internal _mToken; MockWrappedMToken internal _wrappedMToken; MockSpokeRegistrar internal _registrar; + bytes32 internal _remoteWrappedMToken; PortalHarness internal _portal; function setUp() external { _mToken = new MockSpokeMToken(); _wrappedMToken = new MockWrappedMToken(address(_mToken)); + _remoteWrappedMToken = address(_wrappedMToken).toBytes32(); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -84,7 +86,8 @@ contract PortalTests is UnitTestBase { uint128(type(uint64).max) + 1, _alice.toBytes32(), _LOCAL_CHAIN_ID, - _REMOTE_CHAIN_ID + _REMOTE_CHAIN_ID, + address(_mToken).toBytes32() ); } @@ -99,7 +102,8 @@ contract PortalTests is UnitTestBase { index_, recipient_, _LOCAL_CHAIN_ID, - _REMOTE_CHAIN_ID + _REMOTE_CHAIN_ID, + address(_mToken).toBytes32() ); vm.deal(_alice, msgValue_); @@ -137,7 +141,14 @@ contract PortalTests is UnitTestBase { bytes32 refundAddress_ = recipient_; vm.expectRevert(INttManager.ZeroAmount.selector); - _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); } function test_transferWrappedMToken_zeroRecipient() external { @@ -146,7 +157,14 @@ contract PortalTests is UnitTestBase { bytes32 refundAddress_ = _alice.toBytes32(); vm.expectRevert(INttManager.InvalidRecipient.selector); - _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); } function test_transferWrappedMToken_zeroRefundAddress() external { @@ -155,7 +173,14 @@ contract PortalTests is UnitTestBase { bytes32 refundAddress_ = bytes32(0); vm.expectRevert(INttManager.InvalidRefundAddress.selector); - _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); } function test_transferWrappedMToken_unsupportedDestinationToken() external { @@ -164,32 +189,34 @@ contract PortalTests is UnitTestBase { bytes32 refundAddress_ = recipient_; vm.expectRevert( - abi.encodeWithSelector( - IPortal.UnsupportedDestinationToken.selector, - address(_wrappedMToken), - _REMOTE_CHAIN_ID - ) + abi.encodeWithSelector(IPortal.UnsupportedDestinationToken.selector, _REMOTE_CHAIN_ID, _remoteWrappedMToken) ); - _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); } function test_transferWrappedMToken() external { uint256 amount_ = 1_000e6; uint128 index_ = 0; - bytes32 destinationWrappedToken_ = makeAddr("wrapped M").toBytes32(); bytes32 recipient_ = _alice.toBytes32(); bytes32 refundAddress_ = recipient_; - _portal.setDestinationWrappedMToken(address(_wrappedMToken), _REMOTE_CHAIN_ID, destinationWrappedToken_); + _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); - (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( amount_, index_, recipient_, _LOCAL_CHAIN_ID, _REMOTE_CHAIN_ID, - destinationWrappedToken_ + _remoteWrappedMToken ); _mToken.mint(_alice, amount_); @@ -216,12 +243,28 @@ contract PortalTests is UnitTestBase { ); vm.expectEmit(); - emit IPortal.MTokenSent(_REMOTE_CHAIN_ID, messageId_, _alice, recipient_, amount_, index_); + emit IPortal.WrappedMTokenSent( + _REMOTE_CHAIN_ID, + address(_wrappedMToken), + _remoteWrappedMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); vm.expectEmit(); emit INttManager.TransferSent(messageId_); - _portal.transferWrappedMToken(amount_, address(_wrappedMToken), _REMOTE_CHAIN_ID, recipient_, refundAddress_); + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); } /* ============ _handleMsg ============ */ @@ -236,7 +279,8 @@ contract PortalTests is UnitTestBase { index_, recipient_, _LOCAL_CHAIN_ID, - _REMOTE_CHAIN_ID + _REMOTE_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectRevert(abi.encodeWithSelector(IPortal.InvalidFork.selector, 31337, 1)); diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index a40efb3..32e8531 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -156,16 +156,19 @@ contract SpokePortalTests is UnitTestBase { /* ============ _receiveMToken ============ */ function test_receiveMToken_invalidTargetChain() external { + uint16 invalidChainId = 1111; + (TransceiverStructs.NttManagerMessage memory message_, ) = _createTransferMessage( 1_000e6, _EXP_SCALED_ONE, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _REMOTE_CHAIN_ID + invalidChainId, + address(_mToken).toBytes32() ); vm.expectRevert( - abi.encodeWithSelector(INttManager.InvalidTargetChain.selector, _REMOTE_CHAIN_ID, _LOCAL_CHAIN_ID) + abi.encodeWithSelector(INttManager.InvalidTargetChain.selector, invalidChainId, _LOCAL_CHAIN_ID) ); vm.prank(address(_transceiver)); @@ -184,16 +187,17 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); @@ -211,7 +215,8 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); bytes memory call = remoteIndex_ > localIndex_ @@ -237,7 +242,8 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", _alice, amount_)); @@ -259,7 +265,8 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", _alice, amount_)); @@ -281,7 +288,8 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); vm.expectCall( @@ -306,7 +314,8 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_, _alice.toBytes32(), _REMOTE_CHAIN_ID, - _LOCAL_CHAIN_ID + _LOCAL_CHAIN_ID, + address(_mToken).toBytes32() ); bytes memory call = remoteIndex_ > localIndex_ @@ -326,7 +335,7 @@ contract SpokePortalTests is UnitTestBase { _mToken.setCurrentIndex(localIndex_); - (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( amount_, remoteIndex_, _alice.toBytes32(), @@ -339,10 +348,18 @@ contract SpokePortalTests is UnitTestBase { vm.expectCall(address(_wrappedMToken), abi.encodeWithSignature("wrap(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.WrappedMTokenReceived( + _REMOTE_CHAIN_ID, + address(_wrappedMToken), + messageId_, + _alice.toBytes32(), + _alice, + amount_, + remoteIndex_ + ); vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); @@ -356,7 +373,7 @@ contract SpokePortalTests is UnitTestBase { _mToken.setCurrentIndex(localIndex_); - (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createWrappedMTransferMessage( + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( amount_, remoteIndex_, _alice.toBytes32(), @@ -369,10 +386,18 @@ contract SpokePortalTests is UnitTestBase { vm.expectCall(address(_mToken), abi.encodeWithSignature("transfer(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.WrappedMTokenReceived( + _REMOTE_CHAIN_ID, + destinationWrappedToken_, + messageId_, + _alice.toBytes32(), + _alice, + amount_, + remoteIndex_ + ); vm.expectEmit(); emit IPortal.WrapFailed(destinationWrappedToken_, _alice, amount_); diff --git a/test/unit/UnitTestBase.t.sol b/test/unit/UnitTestBase.t.sol index 65fbf60..02e51b9 100644 --- a/test/unit/UnitTestBase.t.sol +++ b/test/unit/UnitTestBase.t.sol @@ -67,23 +67,6 @@ contract UnitTestBase is Test { } function _createTransferMessage( - uint256 amount_, - uint128 index_, - bytes32 recipient_, - uint16 sourceChainId_, - uint16 destinationChainId_ - ) internal view returns (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) { - (message_, messageId_) = _createWrappedMTransferMessage( - amount_, - index_, - recipient_, - sourceChainId_, - destinationChainId_, - bytes32(0) - ); - } - - function _createWrappedMTransferMessage( uint256 amount_, uint128 index_, bytes32 recipient_, From ba0e968aa4bd35626255fb83e29310f90cda7e9a Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Tue, 21 Jan 2025 07:59:23 -0800 Subject: [PATCH 08/10] refactor: merge Sent/Receive events --- src/Portal.sol | 52 ++++++++--- src/SpokePortal.sol | 8 ++ src/interfaces/IPortal.sol | 63 +++++-------- test/unit/HubPortal.t.sol | 158 ++++++++++++++++++++++++++++++- test/unit/Portal.t.sol | 82 +++------------- test/unit/SpokePortal.t.sol | 180 +++++++++++++++++++++++++++++++++--- 6 files changed, 409 insertions(+), 134 deletions(-) diff --git a/src/Portal.sol b/src/Portal.sol index 7658497..5d5f2f2 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -38,6 +38,9 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { mapping(uint16 destinationChainId => mapping(bytes32 destinationToken => bool supported)) public supportedDestinationToken; + /// @inheritdoc IPortal + mapping(uint16 destinationChainId => bytes32 mToken) public destinationMToken; + /* ============ Constructor ============ */ /** @@ -71,6 +74,15 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { /* ============ External Interactive Functions ============ */ + /// @inheritdoc IPortal + function setDestinationMToken(uint16 destinationChainId_, bytes32 mToken_) external onlyOwner { + if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_); + if (mToken_ == bytes32(0)) revert ZeroMToken(); + + destinationMToken[destinationChainId_] = mToken_; + emit DestinationMTokenSet(destinationChainId_, mToken_); + } + /// @inheritdoc IPortal function setSupportedDestinationToken( uint16 destinationChainId_, @@ -112,6 +124,8 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. + _burnOrLock(amount_); + uint64 sequence_ = _useMessageSequence(); uint128 index_ = _currentIndex(); @@ -128,10 +142,10 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { uint256 totalPriceQuote_ = _sendMessage(destinationChainId_, refundAddress_, message_); - // Stack too deep + // Prevent stack too deep uint256 transferAmount_ = amount_; - emit WrappedMTokenSent( + emit MTokenSent( destinationChainId_, sourceToken_, destinationToken_, @@ -166,18 +180,29 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { bytes32 // refundAddress ) internal override returns (TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_) { uint128 index_ = _currentIndex(); + bytes32 destinationMToken_ = destinationMToken[destinationChainId_]; bytes32 messageId_; (nativeTokenTransfer_, , messageId_) = _encodeTokenTransfer( amount_, index_, recipient_, - token.toBytes32(), + destinationMToken_, destinationChainId_, sequence_, sender_ ); - emit MTokenSent(destinationChainId_, messageId_, sender_, recipient_, amount_.untrim(tokenDecimals()), index_); + uint256 untrimmedAmount_ = amount_.untrim(tokenDecimals()); + emit MTokenSent( + destinationChainId_, + token, + destinationMToken_, + messageId_, + sender_, + recipient_, + untrimmedAmount_, + index_ + ); } function _encodeTokenTransfer( @@ -277,23 +302,15 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { // NOTE: Assumes that token.decimals() are the same on all chains. uint256 amount_ = trimmedAmount_.untrim(tokenDecimals()); + emit MTokenReceived(sourceChainId_, destinationToken_, messageId_, sender_, recipient_, amount_, index_); + // Emitting `INttManager.TransferRedeemed` to comply with Wormhole NTT specification. emit TransferRedeemed(messageId_); if (destinationToken_ == token) { - emit MTokenReceived(sourceChainId_, messageId_, sender_, recipient_, amount_, index_); // mints or unlocks M Token to the recipient _mintOrUnlock(recipient_, amount_, index_); } else { - emit WrappedMTokenReceived( - sourceChainId_, - destinationToken_, - messageId_, - sender_, - recipient_, - amount_, - index_ - ); // mints or unlocks M Token to the Portal _mintOrUnlock(address(this), amount_, index_); @@ -341,6 +358,13 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { */ function _mintOrUnlock(address recipient_, uint256 amount_, uint128 index_) internal virtual {} + /** + * @dev HubPortal: locks amount_` M tokens. + * SpokePortal: burns `amount_` M tokens. + * @param amount_ The amount of M tokens to lock/burn. + */ + function _burnOrLock(uint256 amount_) internal virtual {} + /// @dev Returns the current M token index used by the Portal. function _currentIndex() internal view virtual returns (uint128) {} } diff --git a/src/SpokePortal.sol b/src/SpokePortal.sol index 2aa2b0c..3fa250d 100644 --- a/src/SpokePortal.sol +++ b/src/SpokePortal.sol @@ -104,6 +104,14 @@ contract SpokePortal is ISpokePortal, Portal { } } + /** + * @dev Burns M Token. + * @param amount_ The amount of M Token to mint to the recipient. + */ + function _burnOrLock(uint256 amount_) internal override { + ISpokeMTokenLike(token).burn(amount_); + } + /// @dev Returns the current M token index used by the Spoke Portal. function _currentIndex() internal view override returns (uint128) { return ISpokeMTokenLike(mToken()).currentIndex(); diff --git a/src/interfaces/IPortal.sol b/src/interfaces/IPortal.sol index 9244911..d617fe2 100644 --- a/src/interfaces/IPortal.sol +++ b/src/interfaces/IPortal.sol @@ -12,24 +12,6 @@ interface IPortal { /** * @notice Emitted when M token is sent to a destination chain. * @param destinationChainId The Wormhole destination chain ID. - * @param messageId The unique identifier for the sent message. - * @param sender The address that bridged the M tokens via the Portal. - * @param recipient The account receiving tokens on destination chain. - * @param amount The amount of tokens. - * @param index The M token index. - */ - event MTokenSent( - uint16 indexed destinationChainId, - bytes32 messageId, - address indexed sender, - bytes32 indexed recipient, - uint256 amount, - uint128 index - ); - - /** - * @notice Emitted when Wrapped M token is sent to a destination chain. - * @param destinationChainId The Wormhole destination chain ID. * @param sourceToken The address of the token on the source chain. * @param destinationToken The address of the token on the destination chain. * @param messageId The unique identifier for the sent message. @@ -38,7 +20,7 @@ interface IPortal { * @param amount The amount of tokens. * @param index The M token index. */ - event WrappedMTokenSent( + event MTokenSent( uint16 destinationChainId, address indexed sourceToken, bytes32 destinationToken, @@ -51,24 +33,6 @@ interface IPortal { /** * @notice Emitted when M token is received from a source chain. - * @param sourceChainId The Wormhole source chain ID. - * @param messageId The unique identifier for the received message. - * @param sender The account sending tokens. - * @param recipient The account receiving tokens. - * @param amount The amount of tokens. - * @param index The M token index. - */ - event MTokenReceived( - uint16 indexed sourceChainId, - bytes32 messageId, - bytes32 indexed sender, - address indexed recipient, - uint256 amount, - uint128 index - ); - - /** - * @notice Emitted when Wrapped M token is received from a source chain. * @param sourceChainId The Wormhole source chain ID. * @param destinationToken The address of the token on the destination chain. * @param messageId The unique identifier for the received message. @@ -77,7 +41,7 @@ interface IPortal { * @param amount The amount of tokens. * @param index The M token index. */ - event WrappedMTokenReceived( + event MTokenReceived( uint16 sourceChainId, address indexed destinationToken, bytes32 messageId, @@ -95,6 +59,13 @@ interface IPortal { */ event WrapFailed(address indexed destinationWrappedToken, address indexed recipient, uint256 amount); + /** + * @notice Emitted when M token is set for the remote chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param mToken The address of M token on the destination chain. + */ + event DestinationMTokenSet(uint16 indexed destinationChainId, bytes32 mToken); + /** * @notice Emitted when a supported token is set for the remote chain. * @param destinationChainId The Wormhole destination chain ID. @@ -143,6 +114,13 @@ interface IPortal { /// @notice The address of the Registrar contract. function registrar() external view returns (address); + /** + * @notice Returns the address of M token the destination chain. + * @param destinationChainId The Wormhole destination chain ID. + * @return mToken The address of M token the destination chain. + */ + function destinationMToken(uint16 destinationChainId) external view returns (bytes32 mToken); + /** * @notice Indicates whether the provided token is supported on the destination chain. * @param destinationChainId The Wormhole destination chain ID. @@ -156,6 +134,13 @@ interface IPortal { /* ============ Interactive Functions ============ */ + /** + * @notice Sets M token address on the remote chain. + * @param destinationChainId The Wormhole destination chain ID. + * @param mToken The address of M token on the destination chain. + */ + function setDestinationMToken(uint16 destinationChainId, bytes32 mToken) external; + /** * @notice Sets whether the token is supported on the remote chain. * @param destinationChainId The Wormhole destination chain ID. @@ -165,7 +150,7 @@ interface IPortal { function setSupportedDestinationToken(uint16 destinationChainId, bytes32 destinationToken, bool supported) external; /** - * @notice Transfers Wrapped M Token to the destination chain. + * @notice Transfers M or Wrapped M Token to the destination chain. * @param amount The amount of tokens to transfer. * @param sourceToken The address of the token (M or Wrapped M) on the source chain. * @param destinationToken The address of the token (M or Wrapped M) on the destination chain. diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index 6836f78..84958e1 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -22,12 +22,18 @@ contract HubPortalTests is UnitTestBase { using TypeConverter for *; MockHubMToken internal _mToken; + MockWrappedMToken internal _wrappedMToken; + bytes32 internal _remoteMToken; + bytes32 internal _remoteWrappedMToken; MockHubRegistrar internal _registrar; HubPortal internal _portal; function setUp() external { _mToken = new MockHubMToken(); + _wrappedMToken = new MockWrappedMToken(address(_mToken)); + _remoteMToken = address(_mToken).toBytes32(); + _remoteWrappedMToken = address(_wrappedMToken).toBytes32(); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -307,6 +313,144 @@ contract HubPortalTests is UnitTestBase { _portal.transfer{ value: fee_ }(amount_, _REMOTE_CHAIN_ID, _alice.toBytes32()); } + /* ============ transferWrappedMToken ============ */ + + function test_transferWrappedMToken_sourceTokenWrappedM() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + _remoteWrappedMToken + ); + + _mToken.mint(_alice, amount_); + + vm.startPrank(_alice); + _mToken.approve(address(_wrappedMToken), amount_); + amount_ = _wrappedMToken.wrap(_alice, amount_); + _wrappedMToken.approve(address(_portal), amount_); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent( + _REMOTE_CHAIN_ID, + address(_wrappedMToken), + _remoteWrappedMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + + assertEq(_mToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_mToken.balanceOf(address(_portal)), amount_); + assertEq(_wrappedMToken.balanceOf(address(_portal)), 0); + } + + function test_transferWrappedMToken_sourceTokenM() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + _remoteWrappedMToken + ); + + _mToken.mint(_alice, amount_); + + vm.startPrank(_alice); + _mToken.approve(address(_portal), amount_); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent( + _REMOTE_CHAIN_ID, + address(_mToken), + _remoteWrappedMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferWrappedMToken( + amount_, + address(_mToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + + assertEq(_mToken.balanceOf(_alice), 0); + assertEq(_mToken.balanceOf(address(_portal)), amount_); + } + /* ============ receiveMToken ============ */ function test_receiveMToken_invalidTargetChain() external { @@ -341,16 +485,24 @@ contract HubPortalTests is UnitTestBase { _alice.toBytes32(), _REMOTE_CHAIN_ID, _LOCAL_CHAIN_ID, - address(_mToken).toBytes32() + _remoteMToken ); vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_alice, amount_))); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.MTokenReceived( + _REMOTE_CHAIN_ID, + _remoteMToken.toAddress(), + messageId_, + _alice.toBytes32(), + _alice, + amount_, + remoteIndex_ + ); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index 7837d16..85acc00 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -25,6 +25,7 @@ contract PortalTests is UnitTestBase { MockSpokeMToken internal _mToken; MockWrappedMToken internal _wrappedMToken; MockSpokeRegistrar internal _registrar; + bytes32 internal _remoteMToken; bytes32 internal _remoteWrappedMToken; PortalHarness internal _portal; @@ -32,6 +33,7 @@ contract PortalTests is UnitTestBase { function setUp() external { _mToken = new MockSpokeMToken(); _wrappedMToken = new MockWrappedMToken(address(_mToken)); + _remoteMToken = address(_wrappedMToken).toBytes32(); _remoteWrappedMToken = address(_wrappedMToken).toBytes32(); _tokenDecimals = _mToken.decimals(); @@ -97,13 +99,15 @@ contract PortalTests is UnitTestBase { uint256 msgValue_ = 2; bytes32 recipient_ = _alice.toBytes32(); + _portal.setDestinationMToken(_REMOTE_CHAIN_ID, _remoteMToken); + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( amount_, index_, recipient_, _LOCAL_CHAIN_ID, _REMOTE_CHAIN_ID, - address(_mToken).toBytes32() + _remoteMToken ); vm.deal(_alice, msgValue_); @@ -128,7 +132,16 @@ contract PortalTests is UnitTestBase { ); vm.expectEmit(); - emit IPortal.MTokenSent(_REMOTE_CHAIN_ID, messageId_, _alice, recipient_, amount_, index_); + emit IPortal.MTokenSent( + _REMOTE_CHAIN_ID, + address(_mToken), + _remoteMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); _portal.transfer{ value: msgValue_ }(amount_, _REMOTE_CHAIN_ID, recipient_); } @@ -202,71 +215,6 @@ contract PortalTests is UnitTestBase { ); } - function test_transferWrappedMToken() external { - uint256 amount_ = 1_000e6; - uint128 index_ = 0; - bytes32 recipient_ = _alice.toBytes32(); - bytes32 refundAddress_ = recipient_; - - _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); - - (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( - amount_, - index_, - recipient_, - _LOCAL_CHAIN_ID, - _REMOTE_CHAIN_ID, - _remoteWrappedMToken - ); - - _mToken.mint(_alice, amount_); - - vm.startPrank(_alice); - _mToken.approve(address(_wrappedMToken), amount_); - amount_ = _wrappedMToken.wrap(_alice, amount_); - _wrappedMToken.approve(address(_portal), amount_); - - // expect to call sendMessage in Transceiver - vm.expectCall( - address(_transceiver), - 0, - abi.encodeCall( - _transceiver.sendMessage, - ( - _REMOTE_CHAIN_ID, - _emptyTransceiverInstruction, - TransceiverStructs.encodeNttManagerMessage(message_), - _PEER, - recipient_ - ) - ) - ); - - vm.expectEmit(); - emit IPortal.WrappedMTokenSent( - _REMOTE_CHAIN_ID, - address(_wrappedMToken), - _remoteWrappedMToken, - messageId_, - _alice, - recipient_, - amount_, - index_ - ); - - vm.expectEmit(); - emit INttManager.TransferSent(messageId_); - - _portal.transferWrappedMToken( - amount_, - address(_wrappedMToken), - _remoteWrappedMToken, - _REMOTE_CHAIN_ID, - recipient_, - refundAddress_ - ); - } - /* ============ _handleMsg ============ */ function test_handleMsg_invalidFork() external { diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index 32e8531..41f7dbe 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -23,6 +23,8 @@ contract SpokePortalTests is UnitTestBase { MockSpokeMToken internal _mToken; MockWrappedMToken internal _wrappedMToken; + bytes32 internal _remoteMToken; + bytes32 internal _remoteWrappedMToken; MockSpokeRegistrar internal _registrar; SpokePortal internal _portal; @@ -30,6 +32,8 @@ contract SpokePortalTests is UnitTestBase { function setUp() external { _mToken = new MockSpokeMToken(); _wrappedMToken = new MockWrappedMToken(address(_mToken)); + _remoteMToken = address(_mToken).toBytes32(); + _remoteWrappedMToken = address(_wrappedMToken).toBytes32(); _tokenDecimals = _mToken.decimals(); _tokenAddress = address(_mToken); @@ -153,6 +157,152 @@ contract SpokePortalTests is UnitTestBase { _portal.transfer(amount_, _REMOTE_CHAIN_ID, _alice.toBytes32()); } + /* ============ transferWrappedMToken ============ */ + + function test_transferWrappedMToken_sourceTokenWrappedM() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + _remoteWrappedMToken + ); + + _mToken.mint(_alice, amount_); + + vm.startPrank(_alice); + _mToken.approve(address(_wrappedMToken), amount_); + amount_ = _wrappedMToken.wrap(_alice, amount_); + _wrappedMToken.approve(address(_portal), amount_); + + vm.expectCall(address(_mToken), abi.encodeWithSignature("burn(uint256)", amount_)); + vm.expectCall( + address(_wrappedMToken), + abi.encodeWithSignature("unwrap(address,uint256)", address(_portal), amount_) + ); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent( + _REMOTE_CHAIN_ID, + address(_wrappedMToken), + _remoteWrappedMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferWrappedMToken( + amount_, + address(_wrappedMToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + + assertEq(_mToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_mToken.balanceOf(address(_portal)), 0); + assertEq(_wrappedMToken.balanceOf(address(_portal)), 0); + } + + function test_transferWrappedMToken_sourceTokenM() external { + uint256 amount_ = 1_000e6; + uint128 index_ = 0; + bytes32 recipient_ = _alice.toBytes32(); + bytes32 refundAddress_ = recipient_; + + _portal.setSupportedDestinationToken(_REMOTE_CHAIN_ID, _remoteWrappedMToken, true); + + (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createTransferMessage( + amount_, + index_, + recipient_, + _LOCAL_CHAIN_ID, + _REMOTE_CHAIN_ID, + _remoteWrappedMToken + ); + + _mToken.mint(_alice, amount_); + + vm.startPrank(_alice); + _mToken.approve(address(_portal), amount_); + + vm.expectCall(address(_mToken), abi.encodeWithSignature("burn(uint256)", amount_)); + + // expect to call sendMessage in Transceiver + vm.expectCall( + address(_transceiver), + 0, + abi.encodeCall( + _transceiver.sendMessage, + ( + _REMOTE_CHAIN_ID, + _emptyTransceiverInstruction, + TransceiverStructs.encodeNttManagerMessage(message_), + _PEER, + recipient_ + ) + ) + ); + + vm.expectEmit(); + emit IPortal.MTokenSent( + _REMOTE_CHAIN_ID, + address(_mToken), + _remoteWrappedMToken, + messageId_, + _alice, + recipient_, + amount_, + index_ + ); + + vm.expectEmit(); + emit INttManager.TransferSent(messageId_); + + _portal.transferWrappedMToken( + amount_, + address(_mToken), + _remoteWrappedMToken, + _REMOTE_CHAIN_ID, + recipient_, + refundAddress_ + ); + + assertEq(_mToken.balanceOf(_alice), 0); + assertEq(_mToken.balanceOf(address(_portal)), 0); + } + /* ============ _receiveMToken ============ */ function test_receiveMToken_invalidTargetChain() external { @@ -188,16 +338,24 @@ contract SpokePortalTests is UnitTestBase { _alice.toBytes32(), _REMOTE_CHAIN_ID, _LOCAL_CHAIN_ID, - address(_mToken).toBytes32() + _remoteMToken ); vm.expectCall(address(_mToken), abi.encodeWithSignature("mint(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); + emit IPortal.MTokenReceived( + _REMOTE_CHAIN_ID, + _remoteMToken.toAddress(), + messageId_, + _alice.toBytes32(), + _alice, + amount_, + remoteIndex_ + ); vm.expectEmit(); - emit IPortal.MTokenReceived(_REMOTE_CHAIN_ID, messageId_, _alice.toBytes32(), _alice, amount_, remoteIndex_); + emit INttManager.TransferRedeemed(messageId_); vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); @@ -348,10 +506,7 @@ contract SpokePortalTests is UnitTestBase { vm.expectCall(address(_wrappedMToken), abi.encodeWithSignature("wrap(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); - - vm.expectEmit(); - emit IPortal.WrappedMTokenReceived( + emit IPortal.MTokenReceived( _REMOTE_CHAIN_ID, address(_wrappedMToken), messageId_, @@ -361,6 +516,9 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_ ); + vm.expectEmit(); + emit INttManager.TransferRedeemed(messageId_); + vm.prank(address(_transceiver)); _portal.attestationReceived(_REMOTE_CHAIN_ID, _PEER, message_); } @@ -386,10 +544,7 @@ contract SpokePortalTests is UnitTestBase { vm.expectCall(address(_mToken), abi.encodeWithSignature("transfer(address,uint256)", _alice, amount_)); vm.expectEmit(); - emit INttManager.TransferRedeemed(messageId_); - - vm.expectEmit(); - emit IPortal.WrappedMTokenReceived( + emit IPortal.MTokenReceived( _REMOTE_CHAIN_ID, destinationWrappedToken_, messageId_, @@ -399,6 +554,9 @@ contract SpokePortalTests is UnitTestBase { remoteIndex_ ); + vm.expectEmit(); + emit INttManager.TransferRedeemed(messageId_); + vm.expectEmit(); emit IPortal.WrapFailed(destinationWrappedToken_, _alice, amount_); From 5cb74cdad5c43d9b35b4eea309104b8338383724 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Tue, 21 Jan 2025 22:24:30 -0800 Subject: [PATCH 09/10] refactor: override NTTManager _transferEntryPoint function --- .github/example-native-token-transfers.diff | 13 +++ .github/workflows/test-fork.yml | 5 + .github/workflows/test-gas.yml | 5 + .github/workflows/test-sizes.yml | 5 + foundry.toml | 2 +- src/Portal.sol | 118 +++++++++++++------- test/fork/HubPortalFork.t.sol | 6 + test/fork/SpokePortalFork.t.sol | 12 ++ test/fork/SpokeVaultFork.t.sol | 11 ++ test/unit/HubPortal.t.sol | 1 + test/unit/Portal.t.sol | 5 +- test/unit/SpokePortal.t.sol | 2 + 12 files changed, 137 insertions(+), 48 deletions(-) create mode 100644 .github/example-native-token-transfers.diff diff --git a/.github/example-native-token-transfers.diff b/.github/example-native-token-transfers.diff new file mode 100644 index 0000000..2f847f1 --- /dev/null +++ b/.github/example-native-token-transfers.diff @@ -0,0 +1,13 @@ +diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol +index 7ac021b..60e4aee 100644 +--- a/evm/src/NttManager/NttManager.sol ++++ b/evm/src/NttManager/NttManager.sol +@@ -383,7 +383,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { + bytes32 refundAddress, + bool shouldQueue, + bytes memory transceiverInstructions +- ) internal returns (uint64) { ++ ) internal virtual returns (uint64) { + if (amount == 0) { + revert ZeroAmount(); + } diff --git a/.github/workflows/test-fork.yml b/.github/workflows/test-fork.yml index e59f63e..3d40a3a 100644 --- a/.github/workflows/test-fork.yml +++ b/.github/workflows/test-fork.yml @@ -17,6 +17,11 @@ jobs: with: submodules: recursive + - name: Apply patch + run: | + cd lib/example-native-token-transfers + git apply ../../.github/example-native-token-transfers.diff + - name: Setup Node uses: actions/setup-node@v4 diff --git a/.github/workflows/test-gas.yml b/.github/workflows/test-gas.yml index a17869f..ce0c013 100644 --- a/.github/workflows/test-gas.yml +++ b/.github/workflows/test-gas.yml @@ -17,6 +17,11 @@ jobs: with: submodules: recursive + - name: Apply patch + run: | + cd lib/example-native-token-transfers + git apply ../../.github/example-native-token-transfers.diff + - name: Setup Node uses: actions/setup-node@v4 diff --git a/.github/workflows/test-sizes.yml b/.github/workflows/test-sizes.yml index ae2d19f..a0461d9 100644 --- a/.github/workflows/test-sizes.yml +++ b/.github/workflows/test-sizes.yml @@ -17,6 +17,11 @@ jobs: with: submodules: recursive + - name: Apply patch + run: | + cd lib/example-native-token-transfers + git apply ../../.github/example-native-token-transfers.diff + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 diff --git a/foundry.toml b/foundry.toml index b7ad16b..09bf33d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,7 +16,7 @@ via_ir = false [profile.production] build_info = true optimizer = true -optimizer_runs = 800 +optimizer_runs = 1500 sizes = true via_ir = true diff --git a/src/Portal.sol b/src/Portal.sol index 5d5f2f2..e493c2b 100644 --- a/src/Portal.sol +++ b/src/Portal.sol @@ -105,28 +105,85 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { bytes32 recipient_, bytes32 refundAddress_ ) external payable returns (bytes32 messageId_) { - if (amount_ == 0) revert ZeroAmount(); + _verifyTransferArgs(amount_, destinationToken_, recipient_, refundAddress_); + if (sourceToken_ == address(0)) revert ZeroSourceToken(); - if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken(); - if (recipient_ == bytes32(0)) revert InvalidRecipient(); - if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); if (!supportedDestinationToken[destinationChainId_][destinationToken_]) revert UnsupportedDestinationToken(destinationChainId_, destinationToken_); + IERC20 mToken_ = IERC20(token); + uint256 balanceBefore = mToken_.balanceOf(address(this)); + // transfer source token from the sender IERC20(sourceToken_).transferFrom(msg.sender, address(this), amount_); // if the source token isn't M token, unwrap it - if (sourceToken_ != token) { + if (sourceToken_ != address(mToken_)) { amount_ = IWrappedMTokenLike(sourceToken_).unwrap(address(this), amount_); } + // account for potential rounding errors when transferring between earners and non-earners + amount_ = mToken_.balanceOf(address(this)) - balanceBefore; + + (messageId_, ) = _transferMToken( + amount_, + sourceToken_, + destinationToken_, + destinationChainId_, + recipient_, + refundAddress_ + ); + } + + /* ============ Internal/Private Interactive Functions ============ */ + + /// @dev Called from NTTManager `transfer` function to transfer M token + /// Overridden to reduce code duplication, optimize gas cost and prevent Yul stack too deep + function _transferEntryPoint( + uint256 amount_, + uint16 destinationChainId_, + bytes32 recipient_, + bytes32 refundAddress_, + bool, // shouldQueue_ + bytes memory // transceiverInstructions_ + ) internal override returns (uint64 sequence_) { + bytes32 destinationToken_ = destinationMToken[destinationChainId_]; + + _verifyTransferArgs(amount_, destinationToken_, recipient_, refundAddress_); + + IERC20 mToken_ = IERC20(token); + uint256 balanceBefore = mToken_.balanceOf(address(this)); + + // transfer M token from the sender + mToken_.transferFrom(msg.sender, address(this), amount_); + + // account for potential rounding errors when transferring between earners and non-earners + amount_ = mToken_.balanceOf(address(this)) - balanceBefore; + + (, sequence_) = _transferMToken( + amount_, + token, + destinationToken_, + destinationChainId_, + recipient_, + refundAddress_ + ); + } + + function _transferMToken( + uint256 amount_, + address sourceToken_, + bytes32 destinationToken_, + uint16 destinationChainId_, + bytes32 recipient_, + bytes32 refundAddress_ + ) private returns (bytes32 messageId_, uint64 sequence_) { // NOTE: the following code has been adapted from NTT manager `transfer` or `_transferEntryPoint` functions. // We cannot call those functions directly here as they attempt to transfer M Token from the msg.sender. _burnOrLock(amount_); - uint64 sequence_ = _useMessageSequence(); + sequence_ = _useMessageSequence(); uint128 index_ = _currentIndex(); TransceiverStructs.NttManagerMessage memory message_; @@ -167,43 +224,6 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { ); emit TransferSent(messageId_); } - /* ============ Internal/Private Interactive Functions ============ */ - - /// @dev Called from NTT manager during M Token transfer to customize additional payload. - /// Adds M Token index and empty Wrapper Address to the NTT payload. - function _prepareNativeTokenTransfer( - TrimmedAmount amount_, - bytes32 recipient_, - uint16 destinationChainId_, - uint64 sequence_, - address sender_, - bytes32 // refundAddress - ) internal override returns (TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_) { - uint128 index_ = _currentIndex(); - bytes32 destinationMToken_ = destinationMToken[destinationChainId_]; - bytes32 messageId_; - (nativeTokenTransfer_, , messageId_) = _encodeTokenTransfer( - amount_, - index_, - recipient_, - destinationMToken_, - destinationChainId_, - sequence_, - sender_ - ); - - uint256 untrimmedAmount_ = amount_.untrim(tokenDecimals()); - emit MTokenSent( - destinationChainId_, - token, - destinationMToken_, - messageId_, - sender_, - recipient_, - untrimmedAmount_, - index_ - ); - } function _encodeTokenTransfer( TrimmedAmount amount_, @@ -349,6 +369,18 @@ abstract contract Portal is NttManagerNoRateLimiting, IPortal { if (evmChainId_ != block.chainid) revert InvalidFork(evmChainId_, block.chainid); } + function _verifyTransferArgs( + uint256 amount_, + bytes32 destinationToken_, + bytes32 recipient_, + bytes32 refundAddress_ + ) private view { + if (amount_ == 0) revert ZeroAmount(); + if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken(); + if (recipient_ == bytes32(0)) revert InvalidRecipient(); + if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress(); + } + /** * @dev HubPortal: unlocks and transfers `amount_` M tokens to `recipient_`. * SpokePortal: mints `amount_` M tokens to `recipient_`. diff --git a/test/fork/HubPortalFork.t.sol b/test/fork/HubPortalFork.t.sol index 5f85b51..287b7b4 100644 --- a/test/fork/HubPortalFork.t.sol +++ b/test/fork/HubPortalFork.t.sol @@ -9,10 +9,13 @@ import { IMToken } from "../../lib/protocol/src/interfaces/IMToken.sol"; import { IHubPortal } from "../../src/interfaces/IHubPortal.sol"; import { IPortal } from "../../src/interfaces/IPortal.sol"; import { IRegistrarLike } from "../../src/interfaces/IRegistrarLike.sol"; +import { TypeConverter } from "../../src/libs/TypeConverter.sol"; import { ForkTestBase } from "./ForkTestBase.t.sol"; contract HubPortalForkTests is ForkTestBase { + using TypeConverter for *; + function setUp() public override { super.setUp(); _configurePortals(); @@ -31,6 +34,9 @@ contract HubPortalForkTests is ForkTestBase { uint128 mainnetIndex_ = IContinuousIndexing(_MAINNET_M_TOKEN).currentIndex(); + vm.prank(_DEPLOYER); + IPortal(_hubPortal).setDestinationMToken(_BASE_WORMHOLE_CHAIN_ID, _baseSpokeMToken.toBytes32()); + vm.startPrank(_mHolder); vm.recordLogs(); diff --git a/test/fork/SpokePortalFork.t.sol b/test/fork/SpokePortalFork.t.sol index 9bf545a..ce83810 100644 --- a/test/fork/SpokePortalFork.t.sol +++ b/test/fork/SpokePortalFork.t.sol @@ -5,11 +5,14 @@ pragma solidity 0.8.26; import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; import { IContinuousIndexing } from "../../lib/protocol/src/interfaces/IContinuousIndexing.sol"; +import { IPortal } from "../../src/interfaces/IPortal.sol"; import { ISpokePortal } from "../../src/interfaces/ISpokePortal.sol"; +import { TypeConverter } from "../../src/libs/TypeConverter.sol"; import { ForkTestBase } from "./ForkTestBase.t.sol"; contract SpokePortalForkTests is ForkTestBase { + using TypeConverter for *; uint256 internal _amount; uint128 internal _mainnetIndex; @@ -23,6 +26,9 @@ contract SpokePortalForkTests is ForkTestBase { function testFork_transferToHubPortal() external { _beforeTest(); + vm.prank(_DEPLOYER); + IPortal(_baseSpokePortal).setDestinationMToken(_MAINNET_WORMHOLE_CHAIN_ID, _MAINNET_M_TOKEN.toBytes32()); + vm.startPrank(_mHolder); IERC20(_baseSpokeMToken).approve(_baseSpokePortal, _amount); @@ -56,6 +62,9 @@ contract SpokePortalForkTests is ForkTestBase { function testFork_transferBetweenSpokePortals() external { _beforeTest(); + vm.prank(_DEPLOYER); + IPortal(_baseSpokePortal).setDestinationMToken(_OPTIMISM_WORMHOLE_CHAIN_ID, _optimismSpokeMToken.toBytes32()); + vm.startPrank(_mHolder); IERC20(_baseSpokeMToken).approve(_baseSpokePortal, _amount); @@ -90,6 +99,9 @@ contract SpokePortalForkTests is ForkTestBase { // First, transfer M tokens to the Spoke chain. vm.selectFork(_mainnetForkId); + vm.prank(_DEPLOYER); + IPortal(_hubPortal).setDestinationMToken(_BASE_WORMHOLE_CHAIN_ID, _baseSpokeMToken.toBytes32()); + _mainnetIndex = IContinuousIndexing(_MAINNET_M_TOKEN).currentIndex(); vm.startPrank(_mHolder); diff --git a/test/fork/SpokeVaultFork.t.sol b/test/fork/SpokeVaultFork.t.sol index 6efcdb1..fbd9072 100644 --- a/test/fork/SpokeVaultFork.t.sol +++ b/test/fork/SpokeVaultFork.t.sol @@ -4,9 +4,14 @@ pragma solidity 0.8.26; import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; +import { IPortal } from "../../src/interfaces/IPortal.sol"; +import { TypeConverter } from "../../src/libs/TypeConverter.sol"; + import { ForkTestBase } from "./ForkTestBase.t.sol"; contract SpokeVaultForkTests is ForkTestBase { + using TypeConverter for *; + uint256 internal _amount; function setUp() public override { @@ -19,6 +24,9 @@ contract SpokeVaultForkTests is ForkTestBase { function testFork_transferExcessM() external { _beforeTest(); + vm.prank(_DEPLOYER); + IPortal(_baseSpokePortal).setDestinationMToken(_MAINNET_WORMHOLE_CHAIN_ID, _MAINNET_M_TOKEN.toBytes32()); + vm.startPrank(_mHolder); // Then, transfer excess M tokens to the Hub chain. @@ -49,6 +57,9 @@ contract SpokeVaultForkTests is ForkTestBase { vm.selectFork(_mainnetForkId); + vm.prank(_DEPLOYER); + IPortal(_hubPortal).setDestinationMToken(_BASE_WORMHOLE_CHAIN_ID, _MAINNET_M_TOKEN.toBytes32()); + vm.startPrank(_mHolder); // First, transfer M tokens to the Spoke chain diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index 84958e1..7e3044b 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -45,6 +45,7 @@ contract HubPortalTests is UnitTestBase { _portal = HubPortal(_createProxy(address(implementation_))); _initializePortal(_portal); + _portal.setDestinationMToken(_REMOTE_CHAIN_ID, _remoteMToken); } /* ============ initialState ============ */ diff --git a/test/unit/Portal.t.sol b/test/unit/Portal.t.sol index 85acc00..2010da3 100644 --- a/test/unit/Portal.t.sol +++ b/test/unit/Portal.t.sol @@ -50,6 +50,7 @@ contract PortalTests is UnitTestBase { ); _portal = PortalHarness(_createProxy(address(implementation_))); _initializePortal(_portal); + _portal.setDestinationMToken(_REMOTE_CHAIN_ID, _remoteMToken); } /* ============ constructor ============ */ @@ -68,15 +69,11 @@ contract PortalTests is UnitTestBase { function test_transfer_zeroAmount() external { vm.expectRevert(INttManager.ZeroAmount.selector); - - vm.prank(_alice); _portal.transfer(0, _REMOTE_CHAIN_ID, _alice.toBytes32()); } function test_transfer_zeroRecipient() external { vm.expectRevert(INttManager.InvalidRecipient.selector); - - vm.prank(_alice); _portal.transfer(1_000e6, _REMOTE_CHAIN_ID, bytes32(0)); } diff --git a/test/unit/SpokePortal.t.sol b/test/unit/SpokePortal.t.sol index 41f7dbe..888556c 100644 --- a/test/unit/SpokePortal.t.sol +++ b/test/unit/SpokePortal.t.sol @@ -45,6 +45,8 @@ contract SpokePortalTests is UnitTestBase { _portal = SpokePortal(_createProxy(address(implementation_))); _initializePortal(_portal); + + _portal.setDestinationMToken(_REMOTE_CHAIN_ID, _remoteMToken); } /* ============ initialState ============ */ From da911a1f51c306f41bc9f261af9fadb4729ed0f4 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Tue, 21 Jan 2025 22:30:46 -0800 Subject: [PATCH 10/10] ci: apply git patch --- .github/workflows/coverage.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c0d336c..f056d75 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,6 +17,11 @@ jobs: with: submodules: recursive + - name: Apply patch + run: | + cd lib/example-native-token-transfers + git apply ../../.github/example-native-token-transfers.diff + - name: Setup Node uses: actions/setup-node@v4