HubPortal

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/HubPortal.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";
import { TransceiverStructs } from "../lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol";

import { IMTokenLike } from "./interfaces/IMTokenLike.sol";
import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol";
import { IHubPortal } from "./interfaces/IHubPortal.sol";
import { IMerkleTreeBuilder } from "./interfaces/IMerkleTreeBuilder.sol";

import { Portal } from "./Portal.sol";
import { PayloadEncoder } from "./libs/PayloadEncoder.sol";
import { TypeConverter } from "./libs/TypeConverter.sol";

/**
 * @title  Portal residing on Ethereum Mainnet handling sending/receiving M and pushing the M index and Registrar keys.
 * @author M^0 Labs
 */
contract HubPortal is IHubPortal, Portal {
    using TypeConverter for address;

    /* ============ Variables ============ */

    /// @inheritdoc IHubPortal
    bool public wasEarningEnabled;

    /// @inheritdoc IHubPortal
    uint128 public disableEarningIndex;

    /// @inheritdoc IHubPortal
    address public merkleTreeBuilder;

    /* ============ Constructor ============ */

    /**
     * @notice Constructs the contract.
     * @param  mToken_       The address of the M token to bridge.
     * @param  registrar_    The address of the Registrar.
     * @param  swapFacility_ The address of Swap Facility.
     * @param  chainId_      Wormhole chain id.
     */
    constructor(
        address mToken_,
        address registrar_,
        address swapFacility_,
        uint16 chainId_
    ) Portal(mToken_, registrar_, swapFacility_, Mode.LOCKING, chainId_) {}

    /* ============ Interactive Functions ============ */

    /// @inheritdoc IHubPortal
    function sendMTokenIndex(
        uint16 destinationChainId_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) external payable returns (bytes32 messageId_) {
        uint128 index_ = _currentIndex();
        messageId_ = _isSVM(destinationChainId_)
            ? _sendMTokenIndexToSVM(destinationChainId_, index_, refundAddress_, transceiverInstructions_)
            : _sendCustomMessage(
                destinationChainId_,
                refundAddress_,
                PayloadEncoder.encodeIndex(index_, destinationChainId_),
                transceiverInstructions_
            );

        emit MTokenIndexSent(destinationChainId_, messageId_, index_);
    }

    /// @inheritdoc IHubPortal
    function sendRegistrarKey(
        uint16 destinationChainId_,
        bytes32 key_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) external payable returns (bytes32 messageId_) {
        // Sending Registrar key to SVM chains is not supported at this time.
        // To propagate earners to SVM chains call `sendEarnersMerkleRoot`.
        if (_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);

        bytes32 value_ = IRegistrarLike(registrar).get(key_);
        bytes memory payload_ = PayloadEncoder.encodeKey(key_, value_, destinationChainId_);
        messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_, transceiverInstructions_);

        emit RegistrarKeySent(destinationChainId_, messageId_, key_, value_);
    }

    /// @inheritdoc IHubPortal
    function sendRegistrarListStatus(
        uint16 destinationChainId_,
        bytes32 listName_,
        address account_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) external payable returns (bytes32 messageId_) {
        // Sending Registrar list status to SVM chains is not supported at this time.
        // To propagate earners to SVM chains call `sendEarnersMerkleRoot`.
        if (_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);

        bool status_ = IRegistrarLike(registrar).listContains(listName_, account_);
        bytes memory payload_ = PayloadEncoder.encodeListUpdate(listName_, account_, status_, destinationChainId_);
        messageId_ = _sendCustomMessage(destinationChainId_, refundAddress_, payload_, transceiverInstructions_);

        emit RegistrarListStatusSent(destinationChainId_, messageId_, listName_, account_, status_);
    }

    /// @inheritdoc IHubPortal
    function sendEarnersMerkleRoot(
        uint16 destinationChainId_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) external payable returns (bytes32 messageId_) {
        if (!_isSVM(destinationChainId_)) revert UnsupportedDestinationChain(destinationChainId_);

        bytes32 destinationToken_ = destinationMToken[destinationChainId_];
        // TODO: verify if a separate Merkle root needed for each SVM chain
        bytes32 earnersMerkleRoot_ = IMerkleTreeBuilder(merkleTreeBuilder).getRoot(_SOLANA_EARNER_LIST);

        bytes memory additionalPayload_ = PayloadEncoder.encodeAdditionalPayload(
            _currentIndex(),
            destinationToken_,
            earnersMerkleRoot_
        );

        (, messageId_) = _transferNativeToken(
            0,
            token,
            destinationChainId_,
            destinationToken_,
            refundAddress_, // recipient doesn't matter since transfer amount is 0
            refundAddress_,
            additionalPayload_,
            transceiverInstructions_
        );

        emit EarnersMerkleRootSent(destinationChainId_, messageId_, earnersMerkleRoot_);
    }

    /// @inheritdoc IHubPortal
    function setMerkleTreeBuilder(address merkleTreeBuilder_) external onlyOwner {
        if ((merkleTreeBuilder = merkleTreeBuilder_) == address(0)) revert ZeroMerkleTreeBuilder();

        emit MerkleTreeBuilderSet(merkleTreeBuilder_);
    }

    /// @inheritdoc IHubPortal
    function enableEarning() external {
        if (_isEarningEnabled()) revert EarningIsEnabled();
        if (wasEarningEnabled) revert EarningCannotBeReenabled();

        wasEarningEnabled = true;

        IMTokenLike(mToken()).startEarning();

        emit EarningEnabled(IMTokenLike(mToken()).currentIndex());
    }

    /// @inheritdoc IHubPortal
    function disableEarning() external {
        if (!_isEarningEnabled()) revert EarningIsDisabled();

        uint128 currentMIndex_ = IMTokenLike(mToken()).currentIndex();
        disableEarningIndex = currentMIndex_;

        IMTokenLike(mToken()).stopEarning(address(this));

        emit EarningDisabled(currentMIndex_);
    }

    /* ============ Internal Interactive Functions ============ */

    /**
     * @dev   Unlocks M tokens to `recipient_`.
     * @param recipient_ The account to unlock/transfer M tokens to.
     * @param amount_    The amount of M Token to unlock to the recipient.
     */
    function _mintOrUnlock(address recipient_, uint256 amount_, uint128) internal override {
        if (recipient_ != address(this)) {
            IERC20(mToken()).transfer(recipient_, amount_);
        }
    }

    /// @dev Sends a custom (not a transfer) message to the destination chain.
    function _sendCustomMessage(
        uint16 destinationChainId_,
        bytes32 refundAddress_,
        bytes memory payload_,
        bytes memory transceiverInstructions_
    ) private returns (bytes32 messageId_) {
        if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress();

        TransceiverStructs.NttManagerMessage memory message_ = TransceiverStructs.NttManagerMessage(
            bytes32(uint256(_useMessageSequence())),
            msg.sender.toBytes32(),
            payload_
        );

        _sendMessage(destinationChainId_, refundAddress_, message_, transceiverInstructions_);

        messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_);
    }

    /// @dev A workaround to send M Token Index to SVM chains as an additional payload with zero token transfer
    function _sendMTokenIndexToSVM(
        uint16 destinationChainId_,
        uint128 index_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) private returns (bytes32 messageId_) {
        bytes32 destinationToken_ = destinationMToken[destinationChainId_];
        bytes memory additionalPayload_ = PayloadEncoder.encodeAdditionalPayload(index_, destinationToken_);

        (, messageId_) = _transferNativeToken(
            0,
            token,
            destinationChainId_,
            destinationToken_,
            refundAddress_, // recipient doesn't matter since transfer amount is 0
            refundAddress_,
            additionalPayload_,
            transceiverInstructions_
        );
    }

    /* ============ Internal View/Pure Functions ============ */

    /// @dev If earning is enabled returns the current M token index,
    ///      otherwise, returns the index at the time when earning was disabled.
    function _currentIndex() internal view override returns (uint128) {
        return _isEarningEnabled() ? IMTokenLike(mToken()).currentIndex() : disableEarningIndex;
    }

    /// @dev Returns whether earning was enabled for HubPortal or not.
    function _isEarningEnabled() internal view returns (bool) {
        return wasEarningEnabled && disableEarningIndex == 0;
    }
}
"
    },
    "lib/common/src/interfaces/IERC20.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.20 <0.9.0;

/**
 * @title  ERC20 Token Standard.
 * @author M^0 Labs
 * @dev    The interface as defined by EIP-20: https://eips.ethereum.org/EIPS/eip-20
 */
interface IERC20 {
    /* ============ Events ============ */

    /**
     * @notice Emitted when `spender` has been approved for `amount` of the token balance of `account`.
     * @param  account The address of the account.
     * @param  spender The address of the spender being approved for the allowance.
     * @param  amount  The amount of the allowance being approved.
     */
    event Approval(address indexed account, address indexed spender, uint256 amount);

    /**
     * @notice Emitted when `amount` tokens is transferred from `sender` to `recipient`.
     * @param  sender    The address of the sender who's token balance is decremented.
     * @param  recipient The address of the recipient who's token balance is incremented.
     * @param  amount    The amount of tokens being transferred.
     */
    event Transfer(address indexed sender, address indexed recipient, uint256 amount);

    /* ============ Interactive Functions ============ */

    /**
     * @notice Allows a calling account to approve `spender` to spend up to `amount` of its token balance.
     * @dev    MUST emit an `Approval` event.
     * @param  spender The address of the account being allowed to spend up to the allowed amount.
     * @param  amount  The amount of the allowance being approved.
     * @return Whether or not the approval was successful.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @notice Allows a calling account to transfer `amount` tokens to `recipient`.
     * @param  recipient The address of the recipient who's token balance will be incremented.
     * @param  amount    The amount of tokens being transferred.
     * @return Whether or not the transfer was successful.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

    /**
     * @notice Allows a calling account to transfer `amount` tokens from `sender`, with allowance, to a `recipient`.
     * @param  sender    The address of the sender who's token balance will be decremented.
     * @param  recipient The address of the recipient who's token balance will be incremented.
     * @param  amount    The amount of tokens being transferred.
     * @return Whether or not the transfer was successful.
     */
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    /* ============ View/Pure Functions ============ */

    /**
     * @notice Returns the allowance `spender` is allowed to spend on behalf of `account`.
     * @param  account The address of the account who's token balance `spender` is allowed to spend.
     * @param  spender The address of an account allowed to spend on behalf of `account`.
     * @return The amount `spender` can spend on behalf of `account`.
     */
    function allowance(address account, address spender) external view returns (uint256);

    /**
     * @notice Returns the token balance of `account`.
     * @param  account The address of some account.
     * @return The token balance of `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /// @notice Returns the number of decimals UIs should assume all amounts have.
    function decimals() external view returns (uint8);

    /// @notice Returns the name of the contract/token.
    function name() external view returns (string memory);

    /// @notice Returns the symbol of the token.
    function symbol() external view returns (string memory);

    /// @notice Returns the current total supply of the token.
    function totalSupply() external view returns (uint256);
}
"
    },
    "lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
import "./TrimmedAmount.sol";

library TransceiverStructs {
    using BytesParsing for bytes;
    using TrimmedAmountLib for TrimmedAmount;

    /// @notice Error thrown when the payload length exceeds the allowed maximum.
    /// @dev Selector 0xa3419691.
    /// @param size The size of the payload.
    error PayloadTooLong(uint256 size);

    /// @notice Error thrown when the prefix of an encoded message
    ///         does not match the expected value.
    /// @dev Selector 0x56d2569d.
    /// @param prefix The prefix that was found in the encoded message.
    error IncorrectPrefix(bytes4 prefix);

    /// @notice Error thrown when the transceiver instructions aren't
    ///         encoded with strictly increasing indices
    /// @dev Selector 0x0555a4b9.
    /// @param lastIndex Last parsed instruction index
    /// @param instructionIndex The instruction index that was unordered
    error UnorderedInstructions(uint256 lastIndex, uint256 instructionIndex);

    /// @notice Error thrown when a transceiver instruction index
    ///         is greater than the number of registered transceivers
    /// @dev We index from 0 so if providedIndex == numTransceivers then we're out-of-bounds too
    /// @dev Selector 0x689f5016.
    /// @param providedIndex The index specified in the instruction
    /// @param numTransceivers The number of registered transceivers
    error InvalidInstructionIndex(uint256 providedIndex, uint256 numTransceivers);

    /// @dev Prefix for all NativeTokenTransfer payloads
    ///      This is 0x99'N''T''T'
    bytes4 constant NTT_PREFIX = 0x994E5454;

    /// @dev Message emitted and received by the nttManager contract.
    ///      The wire format is as follows:
    ///      - id - 32 bytes
    ///      - sender - 32 bytes
    ///      - payloadLength - 2 bytes
    ///      - payload - `payloadLength` bytes
    struct NttManagerMessage {
        /// @notice unique message identifier
        /// @dev This is incrementally assigned on EVM chains, but this is not
        /// guaranteed on other runtimes.
        bytes32 id;
        /// @notice original message sender address.
        bytes32 sender;
        /// @notice payload that corresponds to the type.
        bytes payload;
    }

    function nttManagerMessageDigest(
        uint16 sourceChainId,
        NttManagerMessage memory m
    ) public pure returns (bytes32) {
        return _nttManagerMessageDigest(sourceChainId, encodeNttManagerMessage(m));
    }

    function _nttManagerMessageDigest(
        uint16 sourceChainId,
        bytes memory encodedNttManagerMessage
    ) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(sourceChainId, encodedNttManagerMessage));
    }

    function encodeNttManagerMessage(
        NttManagerMessage memory m
    ) public pure returns (bytes memory encoded) {
        if (m.payload.length > type(uint16).max) {
            revert PayloadTooLong(m.payload.length);
        }
        uint16 payloadLength = uint16(m.payload.length);
        return abi.encodePacked(m.id, m.sender, payloadLength, m.payload);
    }

    /// @notice Parse a NttManagerMessage.
    /// @param encoded The byte array corresponding to the encoded message
    /// @return nttManagerMessage The parsed NttManagerMessage struct.
    function parseNttManagerMessage(
        bytes memory encoded
    ) public pure returns (NttManagerMessage memory nttManagerMessage) {
        uint256 offset = 0;
        (nttManagerMessage.id, offset) = encoded.asBytes32Unchecked(offset);
        (nttManagerMessage.sender, offset) = encoded.asBytes32Unchecked(offset);
        uint256 payloadLength;
        (payloadLength, offset) = encoded.asUint16Unchecked(offset);
        (nttManagerMessage.payload, offset) = encoded.sliceUnchecked(offset, payloadLength);
        encoded.checkLength(offset);
    }

    /// @dev Native Token Transfer payload.
    ///      The wire format is as follows:
    ///      - NTT_PREFIX - 4 bytes
    ///      - numDecimals - 1 byte
    ///      - amount - 8 bytes
    ///      - sourceToken - 32 bytes
    ///      - to - 32 bytes
    ///      - toChain - 2 bytes
    ///      - additionalPayloadLength - 2 bytes, optional
    ///      - additionalPayload - `additionalPayloadLength` bytes
    struct NativeTokenTransfer {
        /// @notice Amount being transferred (big-endian u64 and u8 for decimals)
        TrimmedAmount amount;
        /// @notice Source chain token address.
        bytes32 sourceToken;
        /// @notice Address of the recipient.
        bytes32 to;
        /// @notice Chain ID of the recipient
        uint16 toChain;
        /// @notice Custom payload
        /// @dev Recommended that the first 4 bytes are a unique prefix
        bytes additionalPayload;
    }

    function encodeNativeTokenTransfer(
        NativeTokenTransfer memory m
    ) public pure returns (bytes memory encoded) {
        // The `amount` and `decimals` fields are encoded in reverse order compared to how they are declared in the
        // `TrimmedAmount` type. This is consistent with the Rust NTT implementation.
        TrimmedAmount transferAmount = m.amount;
        if (m.additionalPayload.length > 0) {
            if (m.additionalPayload.length > type(uint16).max) {
                revert PayloadTooLong(m.additionalPayload.length);
            }
            uint16 additionalPayloadLength = uint16(m.additionalPayload.length);
            return abi.encodePacked(
                NTT_PREFIX,
                transferAmount.getDecimals(),
                transferAmount.getAmount(),
                m.sourceToken,
                m.to,
                m.toChain,
                additionalPayloadLength,
                m.additionalPayload
            );
        }
        return abi.encodePacked(
            NTT_PREFIX,
            transferAmount.getDecimals(),
            transferAmount.getAmount(),
            m.sourceToken,
            m.to,
            m.toChain
        );
    }

    /// @dev Parse a NativeTokenTransfer.
    /// @param encoded The byte array corresponding to the encoded message
    /// @return nativeTokenTransfer The parsed NativeTokenTransfer struct.
    function parseNativeTokenTransfer(
        bytes memory encoded
    ) public pure returns (NativeTokenTransfer memory nativeTokenTransfer) {
        uint256 offset = 0;
        bytes4 prefix;
        (prefix, offset) = encoded.asBytes4Unchecked(offset);
        if (prefix != NTT_PREFIX) {
            revert IncorrectPrefix(prefix);
        }

        // The `amount` and `decimals` fields are parsed in reverse order compared to how they are declared in the
        // `TrimmedAmount` struct. This is consistent with the Rust NTT implementation.
        uint8 numDecimals;
        (numDecimals, offset) = encoded.asUint8Unchecked(offset);
        uint64 amount;
        (amount, offset) = encoded.asUint64Unchecked(offset);
        nativeTokenTransfer.amount = packTrimmedAmount(amount, numDecimals);

        (nativeTokenTransfer.sourceToken, offset) = encoded.asBytes32Unchecked(offset);
        (nativeTokenTransfer.to, offset) = encoded.asBytes32Unchecked(offset);
        (nativeTokenTransfer.toChain, offset) = encoded.asUint16Unchecked(offset);
        // The additional payload may be omitted, but if it is included, it is prefixed by a u16 for its length.
        // If there are at least 2 bytes remaining, attempt to parse the additional payload.
        if (encoded.length >= offset + 2) {
            uint256 payloadLength;
            (payloadLength, offset) = encoded.asUint16Unchecked(offset);
            (nativeTokenTransfer.additionalPayload, offset) =
                encoded.sliceUnchecked(offset, payloadLength);
        }
        encoded.checkLength(offset);
    }

    /// @dev Message emitted by Transceiver implementations.
    ///      Each message includes an Transceiver-specified 4-byte prefix.
    ///      The wire format is as follows:
    ///      - prefix - 4 bytes
    ///      - sourceNttManagerAddress - 32 bytes
    ///      - recipientNttManagerAddress - 32 bytes
    ///      - nttManagerPayloadLength - 2 bytes
    ///      - nttManagerPayload - `nttManagerPayloadLength` bytes
    ///      - transceiverPayloadLength - 2 bytes
    ///      - transceiverPayload - `transceiverPayloadLength` bytes
    struct TransceiverMessage {
        /// @notice Address of the NttManager contract that emitted this message.
        bytes32 sourceNttManagerAddress;
        /// @notice Address of the NttManager contract that receives this message.
        bytes32 recipientNttManagerAddress;
        /// @notice Payload provided to the Transceiver contract by the NttManager contract.
        bytes nttManagerPayload;
        /// @notice Optional payload that the transceiver can encode and use for its own message passing purposes.
        bytes transceiverPayload;
    }

    // @notice Encodes an Transceiver message for communication between the
    //         NttManager and the Transceiver.
    // @param m The TransceiverMessage struct containing the message details.
    // @return encoded The byte array corresponding to the encoded message.
    // @custom:throw PayloadTooLong if the length of transceiverId, nttManagerPayload,
    //         or transceiverPayload exceeds the allowed maximum.
    function encodeTransceiverMessage(
        bytes4 prefix,
        TransceiverMessage memory m
    ) public pure returns (bytes memory encoded) {
        if (m.nttManagerPayload.length > type(uint16).max) {
            revert PayloadTooLong(m.nttManagerPayload.length);
        }
        uint16 nttManagerPayloadLength = uint16(m.nttManagerPayload.length);

        if (m.transceiverPayload.length > type(uint16).max) {
            revert PayloadTooLong(m.transceiverPayload.length);
        }
        uint16 transceiverPayloadLength = uint16(m.transceiverPayload.length);

        return abi.encodePacked(
            prefix,
            m.sourceNttManagerAddress,
            m.recipientNttManagerAddress,
            nttManagerPayloadLength,
            m.nttManagerPayload,
            transceiverPayloadLength,
            m.transceiverPayload
        );
    }

    function buildAndEncodeTransceiverMessage(
        bytes4 prefix,
        bytes32 sourceNttManagerAddress,
        bytes32 recipientNttManagerAddress,
        bytes memory nttManagerMessage,
        bytes memory transceiverPayload
    ) public pure returns (TransceiverMessage memory, bytes memory) {
        TransceiverMessage memory transceiverMessage = TransceiverMessage({
            sourceNttManagerAddress: sourceNttManagerAddress,
            recipientNttManagerAddress: recipientNttManagerAddress,
            nttManagerPayload: nttManagerMessage,
            transceiverPayload: transceiverPayload
        });
        bytes memory encoded = encodeTransceiverMessage(prefix, transceiverMessage);
        return (transceiverMessage, encoded);
    }

    /// @dev Parses an encoded message and extracts information into an TransceiverMessage struct.
    /// @param encoded The encoded bytes containing information about the TransceiverMessage.
    /// @return transceiverMessage The parsed TransceiverMessage struct.
    /// @custom:throw IncorrectPrefix if the prefix of the encoded message does not
    ///         match the expected prefix.
    function parseTransceiverMessage(
        bytes4 expectedPrefix,
        bytes memory encoded
    ) internal pure returns (TransceiverMessage memory transceiverMessage) {
        uint256 offset = 0;
        bytes4 prefix;

        (prefix, offset) = encoded.asBytes4Unchecked(offset);

        if (prefix != expectedPrefix) {
            revert IncorrectPrefix(prefix);
        }

        (transceiverMessage.sourceNttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
        (transceiverMessage.recipientNttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
        uint16 nttManagerPayloadLength;
        (nttManagerPayloadLength, offset) = encoded.asUint16Unchecked(offset);
        (transceiverMessage.nttManagerPayload, offset) =
            encoded.sliceUnchecked(offset, nttManagerPayloadLength);
        uint16 transceiverPayloadLength;
        (transceiverPayloadLength, offset) = encoded.asUint16Unchecked(offset);
        (transceiverMessage.transceiverPayload, offset) =
            encoded.sliceUnchecked(offset, transceiverPayloadLength);

        // Check if the entire byte array has been processed
        encoded.checkLength(offset);
    }

    /// @dev Parses the payload of an Transceiver message and returns
    ///      the parsed NttManagerMessage struct.
    /// @param expectedPrefix The prefix that should be encoded in the nttManager message.
    /// @param payload The payload sent across the wire.
    function parseTransceiverAndNttManagerMessage(
        bytes4 expectedPrefix,
        bytes memory payload
    ) public pure returns (TransceiverMessage memory, NttManagerMessage memory) {
        // parse the encoded message payload from the Transceiver
        TransceiverMessage memory parsedTransceiverMessage =
            parseTransceiverMessage(expectedPrefix, payload);

        // parse the encoded message payload from the NttManager
        NttManagerMessage memory parsedNttManagerMessage =
            parseNttManagerMessage(parsedTransceiverMessage.nttManagerPayload);

        return (parsedTransceiverMessage, parsedNttManagerMessage);
    }

    /// @dev Variable-length transceiver-specific instruction that can be passed by the caller to the nttManager.
    ///      The index field refers to the index of the registeredTransceiver that this instruction should be passed to.
    ///      The serialization format is:
    ///      - index - 1 byte
    ///      - payloadLength - 1 byte
    ///      - payload - `payloadLength` bytes
    struct TransceiverInstruction {
        uint8 index;
        bytes payload;
    }

    function encodeTransceiverInstruction(
        TransceiverInstruction memory instruction
    ) public pure returns (bytes memory) {
        if (instruction.payload.length > type(uint8).max) {
            revert PayloadTooLong(instruction.payload.length);
        }
        uint8 payloadLength = uint8(instruction.payload.length);
        return abi.encodePacked(instruction.index, payloadLength, instruction.payload);
    }

    function parseTransceiverInstructionUnchecked(
        bytes memory encoded,
        uint256 offset
    ) public pure returns (TransceiverInstruction memory instruction, uint256 nextOffset) {
        (instruction.index, nextOffset) = encoded.asUint8Unchecked(offset);
        uint8 instructionLength;
        (instructionLength, nextOffset) = encoded.asUint8Unchecked(nextOffset);
        (instruction.payload, nextOffset) = encoded.sliceUnchecked(nextOffset, instructionLength);
    }

    function parseTransceiverInstructionChecked(
        bytes memory encoded
    ) public pure returns (TransceiverInstruction memory instruction) {
        uint256 offset = 0;
        (instruction, offset) = parseTransceiverInstructionUnchecked(encoded, offset);
        encoded.checkLength(offset);
    }

    /// @dev Encode an array of multiple variable-length transceiver-specific instructions.
    ///      The serialization format is:
    ///      - instructionsLength - 1 byte
    ///      - `instructionsLength` number of serialized `TransceiverInstruction` types.
    function encodeTransceiverInstructions(
        TransceiverInstruction[] memory instructions
    ) public pure returns (bytes memory) {
        if (instructions.length > type(uint8).max) {
            revert PayloadTooLong(instructions.length);
        }
        uint256 instructionsLength = instructions.length;

        bytes memory encoded;
        for (uint256 i = 0; i < instructionsLength; i++) {
            bytes memory innerEncoded = encodeTransceiverInstruction(instructions[i]);
            encoded = bytes.concat(encoded, innerEncoded);
        }
        return abi.encodePacked(uint8(instructionsLength), encoded);
    }

    function parseTransceiverInstructions(
        bytes memory encoded,
        uint256 numRegisteredTransceivers
    ) public pure returns (TransceiverInstruction[] memory) {
        uint256 offset = 0;
        uint256 instructionsLength;
        (instructionsLength, offset) = encoded.asUint8Unchecked(offset);

        // We allocate an array with the length of the number of registered transceivers
        // This gives us the flexibility to not have to pass instructions for transceivers that
        // don't need them
        TransceiverInstruction[] memory instructions =
            new TransceiverInstruction[](numRegisteredTransceivers);

        uint256 lastIndex = 0;
        for (uint256 i = 0; i < instructionsLength; i++) {
            TransceiverInstruction memory instruction;
            (instruction, offset) = parseTransceiverInstructionUnchecked(encoded, offset);

            uint8 instructionIndex = instruction.index;

            // The instructions passed in have to be strictly increasing in terms of transceiver index
            if (i != 0 && instructionIndex <= lastIndex) {
                revert UnorderedInstructions(lastIndex, instructionIndex);
            }

            // Instruction index is out of bounds
            if (instructionIndex >= numRegisteredTransceivers) {
                revert InvalidInstructionIndex(instructionIndex, numRegisteredTransceivers);
            }

            lastIndex = instructionIndex;

            instructions[instructionIndex] = instruction;
        }

        encoded.checkLength(offset);

        return instructions;
    }

    struct TransceiverInit {
        bytes4 transceiverIdentifier;
        bytes32 nttManagerAddress;
        uint8 nttManagerMode;
        bytes32 tokenAddress;
        uint8 tokenDecimals;
    }

    function encodeTransceiverInit(
        TransceiverInit memory init
    ) public pure returns (bytes memory) {
        return abi.encodePacked(
            init.transceiverIdentifier,
            init.nttManagerAddress,
            init.nttManagerMode,
            init.tokenAddress,
            init.tokenDecimals
        );
    }

    function decodeTransceiverInit(
        bytes memory encoded
    ) public pure returns (TransceiverInit memory init) {
        uint256 offset = 0;
        (init.transceiverIdentifier, offset) = encoded.asBytes4Unchecked(offset);
        (init.nttManagerAddress, offset) = encoded.asBytes32Unchecked(offset);
        (init.nttManagerMode, offset) = encoded.asUint8Unchecked(offset);
        (init.tokenAddress, offset) = encoded.asBytes32Unchecked(offset);
        (init.tokenDecimals, offset) = encoded.asUint8Unchecked(offset);
        encoded.checkLength(offset);
    }

    struct TransceiverRegistration {
        bytes4 transceiverIdentifier;
        uint16 transceiverChainId;
        bytes32 transceiverAddress;
    }

    function encodeTransceiverRegistration(
        TransceiverRegistration memory registration
    ) public pure returns (bytes memory) {
        return abi.encodePacked(
            registration.transceiverIdentifier,
            registration.transceiverChainId,
            registration.transceiverAddress
        );
    }

    function decodeTransceiverRegistration(
        bytes memory encoded
    ) public pure returns (TransceiverRegistration memory registration) {
        uint256 offset = 0;
        (registration.transceiverIdentifier, offset) = encoded.asBytes4Unchecked(offset);
        (registration.transceiverChainId, offset) = encoded.asUint16Unchecked(offset);
        (registration.transceiverAddress, offset) = encoded.asBytes32Unchecked(offset);
        encoded.checkLength(offset);
    }
}
"
    },
    "src/interfaces/IMTokenLike.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

/**
 * @title  Subset of M Token interface required for Portal contracts.
 * @author M^0 Labs
 */
interface IMTokenLike {
    /// @notice The current index that would be written to storage if `updateIndex` is called.
    function currentIndex() external view returns (uint128);

    /**
     * @notice Checks if account is an earner.
     * @param  account The account to check.
     * @return True if account is an earner, false otherwise.
     */
    function isEarning(address account) external view returns (bool);

    /// @notice Starts earning for caller if allowed by TTG.
    function startEarning() external;

    /// @notice Stops earning for the account.
    function stopEarning(address account) external;
}
"
    },
    "src/interfaces/IRegistrarLike.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

/**
 * @title  Subset of Registrar interface required for Portal contracts.
 * @author M^0 Labs
 */
interface IRegistrarLike {
    /**
     * @notice Adds `account` to `list`.
     * @param  list    The key for some list.
     * @param  account The address of some account to be added.
     */
    function addToList(bytes32 list, address account) external;

    /**
     * @notice Removes `account` from `list`.
     * @param  list    The key for some list.
     * @param  account The address of some account to be removed.
     */
    function removeFromList(bytes32 list, address account) external;

    /**
     * @notice Sets `key` to `value`.
     * @param  key   Some key.
     * @param  value Some value.
     */
    function setKey(bytes32 key, bytes32 value) external;

    /**
     * @notice Returns the value of `key`.
     * @param  key Some key.
     * @return Some value.
     */
    function get(bytes32 key) external view returns (bytes32);

    /**
     * @notice Returns whether `list` contains `account`.
     * @param  list    The key for some list.
     * @param  account The address of some account.
     * @return Whether `list` contains `account`.
     */
    function listContains(bytes32 list, address account) external view returns (bool);

    /// @notice Returns the address of the Portal contract.
    function portal() external view returns (address);
}
"
    },
    "src/interfaces/IHubPortal.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

import { IPortal } from "./IPortal.sol";

/**
 * @title  HubPortal interface.
 * @author M^0 Labs
 */
interface IHubPortal is IPortal {
    /* ============ Events ============ */

    /**
     * @notice Emitted when earning is enabled for the Hub Portal.
     * @param  index The index at which earning was enabled.
     */
    event EarningEnabled(uint128 index);

    /**
     * @notice Emitted when earning is disabled for the Hub Portal.
     * @param  index The index at which earning was disabled.
     */
    event EarningDisabled(uint128 index);

    /**
     * @notice Emitted when the M token index is sent to a destination chain.
     * @param  destinationChainId The Wormhole destination chain ID.
     * @param  messageId          The unique identifier for the sent message.
     * @param  index              The the M token index.
     */
    event MTokenIndexSent(uint16 indexed destinationChainId, bytes32 messageId, uint128 index);

    /**
     * @notice Emitted when the Registrar key is sent to a destination chain.
     * @param  destinationChainId The Wormhole destination chain ID.
     * @param  messageId          The unique identifier for the sent message.
     * @param  key                The key that was sent.
     * @param  value              The value that was sent.
     */
    event RegistrarKeySent(uint16 indexed destinationChainId, bytes32 messageId, bytes32 indexed key, bytes32 value);

    /**
     * @notice Emitted when the Registrar list status for an account is sent to a destination chain.
     * @param  destinationChainId The Wormhole destination chain ID.
     * @param  messageId          The unique identifier for the sent message.
     * @param  listName           The name of the list.
     * @param  account            The account.
     * @param  status             The status of the account in the list.
     */
    event RegistrarListStatusSent(
        uint16 indexed destinationChainId,
        bytes32 messageId,
        bytes32 indexed listName,
        address indexed account,
        bool status
    );

    /**
     * @notice Emitted when Merkle Tree Builder contract is set.
     * @param  merkleTreeBuilder The address of Merkle Tree Builder contract.
     */
    event MerkleTreeBuilderSet(address merkleTreeBuilder);

    /**
     * @notice Emitted when earners Merkle root is sent to Solana.
     * @param  destinationChainId The Wormhole chain ID for the destination.
     * @param  messageId          The unique identifier for the sent message.
     * @param  earnersMerkleRoot  The Merkle root of earners.
     */
    event EarnersMerkleRootSent(uint16 indexed destinationChainId, bytes32 messageId, bytes32 earnersMerkleRoot);

    /* ============ Custom Errors ============ */

    /// @notice Emitted when trying to enable earning after it has been explicitly disabled.
    error EarningCannotBeReenabled();

    /// @notice Emitted when performing an operation that is not allowed when earning is disabled.
    error EarningIsDisabled();

    /// @notice Emitted when performing an operation that is not allowed when earning is enabled.
    error EarningIsEnabled();

    /// @notice Emitted when calling `setMerkleTreeBuilder` if Merkle Tree Builder address is 0x0.
    error ZeroMerkleTreeBuilder();

    /// @notice Emitted when the destination chain is not supported
    error UnsupportedDestinationChain(uint16 destinationChainId);

    /* ============ View/Pure Functions ============ */

    /// @notice Indicates whether earning for HubPortal was ever enabled.
    function wasEarningEnabled() external returns (bool);

    /// @notice Returns the value of M Token index when earning for HubPortal was disabled.
    function disableEarningIndex() external returns (uint128);

    /// @notice Returns the address of Merkle tree builder.
    function merkleTreeBuilder() external returns (address);

    /* ============ Interactive Functions ============ */

    /**
     * @notice Sends the M token index to the destination chain.
     * @param  destinationChainId      The Wormhole destination chain ID.
     * @param  refundAddress           The refund address to receive excess native gas.
     * @param  transceiverInstructions The transceiver specific instructions for quoting and sending.
     * @return messageId               The ID uniquely identifying the message.
     */
    function sendMTokenIndex(
        uint16 destinationChainId,
        bytes32 refundAddress,
        bytes memory transceiverInstructions
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Sends the Registrar key to the destination chain.
     * @dev    Not supported for SVM chains.
     * @param  destinationChainId      The Wormhole destination chain ID.
     * @param  key                     The key to dispatch.
     * @param  refundAddress           The refund address to receive excess native gas.
     * @param  transceiverInstructions The transceiver specific instructions for quoting and sending.
     * @return messageId               The ID uniquely identifying the message
     */
    function sendRegistrarKey(
        uint16 destinationChainId,
        bytes32 key,
        bytes32 refundAddress,
        bytes memory transceiverInstructions
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Sends the Registrar list status for an account to the destination chain.
     * @dev    Not supported for SVM chains.
     * @param  destinationChainId      The Wormhole destination chain ID.
     * @param  listName                The name of the list.
     * @param  account                 The account.
     * @param  refundAddress           The refund address to receive excess native gas.
     * @param  transceiverInstructions The transceiver specific instructions for quoting and sending.
     * @return messageId               The ID uniquely identifying the message.
     */
    function sendRegistrarListStatus(
        uint16 destinationChainId,
        bytes32 listName,
        address account,
        bytes32 refundAddress,
        bytes memory transceiverInstructions
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Sends earners Merkle root to SVM chains.
     * @param  destinationChainId      The Wormhole destination chain ID.
     * @param  refundAddress           The refund address to receive excess native gas.
     * @param  transceiverInstructions The transceiver specific instructions for quoting and sending.
     * @return messageId               The ID uniquely identifying the message.
     */
    function sendEarnersMerkleRoot(
        uint16 destinationChainId,
        bytes32 refundAddress,
        bytes memory transceiverInstructions
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Sets Merkle Tree Builder contract.
     * @param  merkleTreeBuilder The address of Merkle Tree Builder contract.
     */
    function setMerkleTreeBuilder(address merkleTreeBuilder) external;

    /// @notice Enables earning for the Hub Portal if allowed by TTG.
    function enableEarning() external;

    /// @notice Disables earning for the Hub Portal if disallowed by TTG.
    function disableEarning() external;
}
"
    },
    "src/interfaces/IMerkleTreeBuilder.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

/**
 * @title  MerkleTreeBuilder interface.
 * @author M^0 Labs
 * @dev    This contract allows constructing Merkle Trees from values set on the TTGRegistrar.
 *         The reason for this is to allow propagating these values, via the Merkle tree roots,
 *         to other chains in a way that is efficient and trustless.
 */
interface IMerkleTreeBuilder {
    /* ========== ERRORS ========== */

    error ListAlreadyExists();
    error InvalidList();
    error InvalidAdd();
    error InvalidRemove();
    error NotInList();
    error ValueInList();
    error ValueNotInList();

    /* ========== EVENTS ========== */

    event RootUpdated(bytes32 indexed list, bytes32 root);

    /* ========== MANAGE LISTS ========== */

    /**
     * @notice Adds a value to a list.
     * @dev    The value must be included in the list on the TTGRegistrar.
     *         All registrar values are bytes32 => bytes32 mappings.
     *         In the case of lists, the key is the hash of abi.encodePacked("VALUE", list, value).
     * @param  list   The list to add the value to.
     * @param  before The value immediately before the position in the list where the new value should be inserted.
     * @param  value  The value to add to the list.
     */
    function addToList(bytes32 list, bytes32 before, bytes32 value) external;

    /**
     * @notice Removes a value from a list.
     * @dev    The value must have been removed from the list on the TTGRegistrar.
     * @param  list   The list to remove the value from.
     * @param  before The value immediately before the value in list.
     * @param  value  The value to remove from the list.
     */
    function removeFromList(bytes32 list, bytes32 before, bytes32 value) external;

    /* ========== MERKLE TREE ========== */

    /**
     * @notice Updates the Merkle tree root for a list and stores it for later retrieval.
     * @dev    This should be called after adding or removing values from the list.
     * @param  list The list to update the root for.
     */
    function updateRoot(bytes32 list) external;

    /* ========== VIEWS ========== */

    /**
     * @notice Retrieves the value following the provided value in the list.
     * @dev    This is useful for iterating over the list off-chain.
     */
    function getNext(bytes32 list, bytes32 value) external view returns (bytes32);

    /**
     * @notice Retrieves the length of the list.
     * @dev    This is useful for iterating over the list off-chain.
     */
    function getLen(bytes32 list) external view returns (uint256);

    /**
     * @notice Retrieves the root of the Merkle tree for a list.
     */
    function getRoot(bytes32 list) external view returns (bytes32);

    /**
     * @notice Retrieves the list of values in the provided list.
     * @dev    This is useful for retrieving smaller lists off-chain in one go.
     *         Larger lists may run into the gas limit at which point
     *         the list should be retrieved using `getNext`.
     */
    function getList(bytes32 list) external view returns (bytes32[] memory);

    /**
     * @notice Returns whether or not a value is in the provided list on this contract.
     */
    function contains(bytes32 list, bytes32 value) external view returns (bool);
}
"
    },
    "src/Portal.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";
import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol";
import { TrimmedAmount, TrimmedAmountLib } from "../lib/native-token-transfers/evm/src/libraries/TrimmedAmount.sol";
import { TransceiverStructs } from "../lib/native-token-transfers/evm/src/libraries/TransceiverStructs.sol";
import {
    NttManagerNoRateLimiting
} from "../lib/native-token-transfers/evm/src/NttManager/NttManagerNoRateLimiting.sol";

import { IPortal } from "./interfaces/IPortal.sol";
import { ISwapFacilityLike } from "./interfaces/ISwapFacilityLike.sol";
import { TypeConverter } from "./libs/TypeConverter.sol";
import { PayloadType, PayloadEncoder } from "./libs/PayloadEncoder.sol";
import { ReentrancyLock } from "../lib/uniswap-v4-periphery/src/base/ReentrancyLock.sol";

/**
 * @title  Base Portal contract inherited by HubPortal and SpokePortal.
 * @author M^0 Labs
 */
abstract contract Portal is NttManagerNoRateLimiting, ReentrancyLock, IPortal {
    using TypeConverter for *;
    using PayloadEncoder for bytes;
    using TrimmedAmountLib for *;

    uint16 internal constant _SOLANA_WORMHOLE_CHAIN_ID = 1;
    uint16 internal constant _FOGO_WORMHOLE_CHAIN_ID = 51;
    bytes32 internal constant _SOLANA_EARNER_LIST = bytes32("solana-earners");

    /// @inheritdoc IPortal
    address public immutable registrar;

    /// @inheritdoc IPortal
    address public immutable swapFacility;

    /// @inheritdoc IPortal
    mapping(address sourceToken => mapping(uint16 destinationChainId => mapping(bytes32 destinationToken => bool supported)))
        public supportedBridgingPath;

    /// @inheritdoc IPortal
    mapping(uint16 destinationChainId => bytes32 mToken) public destinationMToken;

    /* ============ Constructor ============ */

    /**
     * @notice Constructs the contract.
     * @param  mToken_       The address of the M token to bridge.
     * @param  registrar_    The address of the Registrar.
     * @param  swapFacility_ The address of Swap Facility.
     * @param  mode_         The NttManager token transfer mode - LOCKING or BURNING.
     * @param  chainId_      The Wormhole chain id.
     */
    constructor(
        address mToken_,
        address registrar_,
        address swapFacility_,
        Mode mode_,
        uint16 chainId_
    ) NttManagerNoRateLimiting(mToken_, mode_, chainId_) {
        if (mToken_ == address(0)) revert ZeroMToken();
        if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
        if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility();
    }

    /* ============ View/Pure Functions ============ */

    /// @inheritdoc IPortal
    function mToken() public view returns (address) {
        return token;
    }

    /// @inheritdoc IPortal
    function currentIndex() external view returns (uint128) {
        return _currentIndex();
    }

    /// @inheritdoc IPortal
    function msgSender() external view returns (address) {
        return _getLocker();
    }

    /* ============ 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 setSupportedBridgingPath(
        address sourceToken_,
        uint16 destinationChainId_,
        bytes32 destinationToken_,
        bool supported_
    ) external onlyOwner {
        if (sourceToken_ == address(0)) revert ZeroSourceToken();
        if (destinationChainId_ == chainId) revert InvalidDestinationChain(destinationChainId_);
        if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken();

        supportedBridgingPath[sourceToken_][destinationChainId_][destinationToken_] = supported_;
        emit SupportedBridgingPathSet(sourceToken_, destinationChainId_, destinationToken_, supported_);
    }

    /// @inheritdoc IPortal
    function transferMLikeToken(
        uint256 amount_,
        address sourceToken_,
        uint16 destinationChainId_,
        bytes32 destinationToken_,
        bytes32 recipient_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) external payable isNotLocked whenNotPaused returns (uint64 sequence_) {
        if (!supportedBridgingPath[sourceToken_][destinationChainId_][destinationToken_]) {
            revert UnsupportedBridgingPath(sourceToken_, destinationChainId_, destinationToken_);
        }

        sequence_ = _transferMLikeToken(
            amount_,
            sourceToken_,
            destinationChainId_,
            destinationToken_,
            recipient_,
            refundAddress_,
            transceiverInstructions_
        );
    }

    /* ============ Internal/Private Interactive Functions ============ */

    /**
     * @dev    Called from NTTManager `transfer` function to transfer M token
     * @dev    Overridden to reduce code duplication, optimize gas cost and prevent Yul stack too deep
     * @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 sequence_           The message sequence.
     */
    function _transferEntryPoint(
        uint256 amount_,
        uint16 destinationChainId_,
        bytes32 recipient_,
        bytes32 refundAddress_,
        bool, // shouldQueue_
        bytes memory transceiverInstructions_
    ) internal override isNotLocked returns (uint64 sequence_) {
        sequence_ = _transferMLikeToken(
            amount_,
            token, // M Token
            destinationChainId_,
            destinationMToken[destinationChainId_], // M Token on destination
            recipient_,
            refundAddress_,
            transceiverInstructions_
        );
    }

    /**
     * @dev    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  destinationChainId_      The Wormhole destination chain ID.
     * @param  destinationToken_        The address of the token (M or Wrapped M) on the destination chain.
     * @param  recipient_               The account to receive tokens.
     * @param  refundAddress_           The address to receive excess native gas on the destination chain.
     * @param  transceiverInstructions_ The transceiver specific instructions for quoting and sending.
     * @return sequence_                The message sequence.
     */
    function _transferMLikeToken(
        uint256 amount_,
        address sourceToken_,
        uint16 destinationChainId_,
        bytes32 destinationToken_,
        bytes32 recipient_,
        bytes32 refundAddress_,
        bytes memory transceiverInstructions_
    ) private returns (uint64 sequence_) {
        _verifyTransferAmount(amount_);

        if (destinationToken_ == bytes32(0)) revert ZeroDestinationToken();
        if (recipient_ == bytes32(0)) revert InvalidRecipient();
        if (refundAddress_ == bytes32(0)) revert InvalidRefundAddress();

        IERC20 mToken_ = IERC20(token);
        uint256 startingBalance_ = 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_ != address(mToken_)) {
            IERC20(sourceToken_).approve(swapFacility, amount_);
            ISwapFacilityLike(swapFacility).swapOutM(sourceToken_, amount_, address(this));
        }

        // The actual amount of M tokens that Portal received from the sender.
        // Accounts for potential rounding errors when transferring between earners and non-earners,
        // as well as potential fee-on-transfer functionality in the source token.
        uint256 actualAmount_ = mToken_.balanceOf(address(this)) - startingBalance_;

        if (amount_ > actualAmount_) {
            unchecked {
                // If the difference between the specified transfer amount and the actual amount exceeds
                // the maximum acceptable rounding error (e.g., due to fee-on-transfer in an extension token)
                // transfer the actual amount, not the specified.

                // Otherwise, the specified amount will be transferred and the deficit caused by rounding error will
                // be covered from the yield earned by HubPortal.
                if (amount_ - actualAmount_ > _getMaxRoundingError()) {
                    amount_ = actualAmount_;
                    _verifyTransferAmount(amount_);
                }
            }
        }

        // Burn the actual amount of M tokens on Spoke.
        // In case of Hub, do nothing, as tokens are already transferred.
        _burnOrLock(actualAmount_);

        // Prevent stack too deep error
        bytes32 destinationToken = destinationToken_;

        (sequence_, ) = _transferNativeToken(
            amount_,
            sourceToken_,
            destinationChainId_,
            destinationToken,
            recipient_,
            refundAddress_,
            PayloadEncoder.encodeAdditionalPayload(_currentIndex(), destinationToken),
            transceiverInstructions_
        );
    }

    /**
     * @dev    Transfers M or Wrapped M Token to the destination chain.
     * @dev    adapted from NttManager `_transfer` function.
     * @dev    https://github.com/wormhole-foundation/native-token-transfers/blob/main/evm/src/NttManager/NttManager.sol#L521
     * @param  amount_                  The amount of tokens to transfer.
     * @param  sourceToken_             The address of the token (M or Wrapped M) on the source chain.
     * @param  destinationChainId_      The Wormhole destination chain ID.
     * @param  destinationToken_        The address of the token (M or Wrapped M) on the destination chain.
     * @param  recipient_               The account to receive tokens.
     * @param  refundAddress_           The address to receive excess native gas on the destination chain.
     * @param  additionalPayload_       The additional payload to sent with tokens transfer.
     * @param  transceiverInstructions_ The transceiver specific instructions for quoting and sending.
     * @return sequence_                The message sequence.
     */
    function _transferNativeToken(
        uint256 amount_,
        address sourceToken_,
        uint16 destinationChainId_,
        bytes32 destinationToken_,
        bytes32 recipient_,
        bytes32 refundAddress_,
        bytes memory additionalPayload_,
        bytes memory transceiverInstructions_
    ) internal returns (uint64 sequence_, bytes32 messageId_) {
        sequence_ = _useMessageSequence();
        TransceiverStructs.NttManagerMessage memory message_;
        (message_, messageId_) = _encodeTokenTransfer(
            _trimTransferAmount(amount_, destinationChainId_),
            destinationChainId_,
            msg.sender,
            recipient_,
            additionalPayload_,
            sequence_
        );

        uint256 totalPriceQuote_ = _sendMessage(
            destinationChainId_,
            refundAddress_,
            message_,
            transceiverInstructions_
        );

        // prevent stack too deep
        uint256 transferAmount_ = amount_;

        emit MTokenSent(
            sourceToken_,
            destinationChainId_,
            destinationToken_,
            msg.sender,
            recipient_,
            transferAmount_,
            _currentIndex(),
            messageId_
        );

        // emit NTT events
        emit TransferSent(
            recipient_,
            refundAddress_,
            transferAmount_,
            totalPriceQuote_,
            destinationChainId_,
            sequence_
        );
        emit TransferSent(messageId_);
    }

    /**
     * @dev    Encodes transfer information into NTT format.
     * @param  amount_             The amount of tokens to transfer.
     * @param  destinationChainId_ The Wormhole destination chain ID.
     * @param  sender_             The message sender.
     * @param  recipient_          The account to receive tokens.
     * @param  additionalPayload_  The additional payload to send with tokens transfer.
     * @param  sequence_           The message sequence.
     * @return message_            The message in NTT format.
     * @return messageId_          The message Id.
     */
    function _encodeTokenTransfer(
        TrimmedAmount amount_,
        uint16 destinationChainId_,
        address sender_,
        bytes32 recipient_,
        bytes memory additionalPayload_,
        uint64 sequence_
    ) internal view returns (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) {
        TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer_ = TransceiverStructs.NativeTokenTransfer(
            amount_,
            token.toBytes32(),
            recipient_,
            destinationChainId_,
            additionalPayload_
        );

        message_ = TransceiverStructs.NttManagerMessage(
            bytes32(uint256(sequence_)),
            sender_.toBytes32(),
            TransceiverStructs.encodeNativeTokenTransfer(nativeTokenTransfer_)
        );

        messageId_ = TransceiverStructs.nttManagerMessageDigest(chainId, message_);
    }

    /**
     * @dev    Sends a generic message to the destination chain.
     *         The implementation is adapted from `NttManager` `_transfer` function.
     * @param  destinationChainId_      The Wormhole destination chain ID.
     * @param  refundAddress_           The address to receive excess native gas on the destination chain.
     * @param  message_                 The message to send.
     * @param  transceiverInstructions_ The transceiver specific instructions for quoting and sending.
     * @return totalPriceQuote_         The price to deliver the message to the destination chain.
     */
    function _sendMessage(
        uint16 destinationChainId_,
        bytes32 refundAddress_,
        TransceiverStructs.NttManagerMessage memory message_,
        bytes memory transceiverInstructions_
    ) internal returns (uint256 totalPriceQuote_) {
        _verifyIfChainForked();

        address[] memory enabledTransceivers_;
        TransceiverStructs.TransceiverInstruction[] memory instructions_;
        uint256[] memory priceQuotes_;

        (enabledTransceivers_, instructions_, priceQuotes_, totalPriceQuote_) = _prepareForTransfer(
            destinationChainId_,
            transceiverInstructions_
        );

        // send a message
        _sendMessageToTransceivers(
            destinationChainId_,
            refundAddress_,
            _getPeersStorage()[destinationChainId_].peerAddress,
            priceQuotes_,
            instructions_,
            enabledTransceivers_,
            TransceiverStructs.encodeNttManagerMessage(message_)
        );
    }

    /**
     * @dev    Handles token transfer with an additional payload and custom payload types on the destination.
     * @param  sourceChainId_ The Wormhole source chain ID.
     * @param  message_       The message.
     */
    function _handleMsg(
        uint16 sourceChainId_,
        bytes32, // sourceNttManagerAddress
        TransceiverStructs.NttManagerMessage memory message_,
        bytes32 messageId_ // digest
    ) internal override {
        bytes memory payload_ = message_.payload;
        PayloadType payloadType_ = message_.payload.getPayloadType();

        _verifyIfChainForked();

        if (payloadType_ == PayloadType.Token) {
            _receiveMToken(sourceChainId_, messageId_, message_.sender, payload_);
            return;
        }

        _receiveCustomPayload(messageId_, payloadType_, payload_);
    }

    /**
     * @dev   Handles token transfer message on the destination.
     * @param sourceChainId_ The Wormhole source chain ID.
     * @param messageId_     The message ID.
     * @param sender_        The address of the message sender.
     * @param payload_       The message payload.
     */
    function _receiveMToken(uint16 sourceChainId_, bytes32 messageId_, bytes32 sender_, bytes memory payload_) private {
        (
            TrimmedAmount trimmedAmount_,
            uint128 index_,
            address destinationToken_,
            address recipient_,
            uint16 destinationChainId_
        ) = payload_.decodeTokenTransfer();

        _verifyDestinationChain(destinationChainId_);

        // NOTE: Assumes that token.decimals() are the same on all chains.
        uint256 amount_ = trimmedAmount_.untrim(tokenDecimals());

        emit MTokenReceived(sourceChainId_, destinationToken_, sender_, recipient_, amount_, index_, messageId_);

        // Emitting `INttManager.TransferRedeemed` to

Tags:
ERC20, Multisig, Mintable, Burnable, Pausable, Swap, Yield, Voting, Upgradeable, Multi-Signature, Factory|addr:0xce0b26c62a4c0c87ed8dec26d554c169be1d1a5b|verified:true|block:23492258|tx:0x90fbe2ebbb1e4436d10e6eaa7d75962c14de5859db650a801d9f848590e6e789|first_check:1759475950

Submitted on: 2025-10-03 09:19:11

Comments

Log in to comment.

No comments yet.