From 03ef8d12adeac47ce2ba5ecce0088440da9be355 Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Mon, 25 Nov 2024 21:33:43 -0800 Subject: [PATCH] feat: smart M bridging --- lib/example-native-token-transfers | 2 +- 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 +++-- 24 files changed, 560 insertions(+), 93 deletions(-) create mode 100644 src/interfaces/IWrappedMTokenLike.sol create mode 100644 test/mocks/MockWrappedMToken.sol diff --git a/lib/example-native-token-transfers b/lib/example-native-token-transfers index e84cbc1..a1c11ef 160000 --- a/lib/example-native-token-transfers +++ b/lib/example-native-token-transfers @@ -1 +1 @@ -Subproject commit e84cbc1f5aa20fba646146d3e8fd919b25f73499 +Subproject commit a1c11ef5958113b6a00b93967e8cef54c887a639 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); }