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 { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol";

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

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

/**
 * @title  HubPortal
 * @author M^0 Labs
 * @notice Deployed on Ethereum Mainnet and responsible for sending and receiving M tokens
 *         as well as propagating M token index, Registrar keys and list status to the Spoke chain.
 * @dev    Tokens are bridged using lock-release mechanism.
 */
contract HubPortal is Portal, IHubPortal {
    /// @inheritdoc IHubPortal
    bool public wasEarningEnabled;

    /// @inheritdoc IHubPortal
    uint128 public disableEarningIndex;

    /// @inheritdoc IHubPortal
    mapping(uint256 spokeChainId => uint256 principal) public bridgedPrincipal;

    /// @inheritdoc IHubPortal
    mapping(uint256 spokeChainId => bool enabled) public crossSpokeConnectionEnabled;

    /**
     * @notice Constructs HubPortal Implementation contract
     * @dev    Sets immutable storage.
     * @param  mToken_    The address of M token.
     * @param  registrar_ The address of Registrar.
     * @param  swapFacility_ The address of Swap Facility.
     */
    constructor(address mToken_, address registrar_, address swapFacility_) Portal(mToken_, registrar_, swapFacility_) { }

    /// @inheritdoc IPortal
    function initialize(address bridge_, address initialOwner_, address initialPauser_) external initializer {
        _initialize(bridge_, initialOwner_, initialPauser_);
        disableEarningIndex = IndexingMath.EXP_SCALED_ONE;
    }

    ///////////////////////////////////////////////////////////////////////////
    //                     EXTERNAL VIEW/PURE FUNCTIONS                      //
    ///////////////////////////////////////////////////////////////////////////

    /// @inheritdoc IHubPortal
    function quoteSendIndex(uint256 destinationChainId_) external view returns (uint256 fee) {
        bytes memory payload_ = PayloadEncoder.encodeIndex(_currentIndex());
        return IBridge(bridge).quote(destinationChainId_, payloadGasLimit[destinationChainId_][PayloadType.Index], payload_);
    }

    /// @inheritdoc IHubPortal
    function quoteSendRegistrarKey(uint256 destinationChainId_, bytes32 key_) external view returns (uint256 fee_) {
        bytes32 value_ = IRegistrarLike(registrar).get(key_);
        bytes memory payload_ = PayloadEncoder.encodeKey(key_, value_);
        return IBridge(bridge).quote(destinationChainId_, payloadGasLimit[destinationChainId_][PayloadType.Key], payload_);
    }

    /// @inheritdoc IHubPortal
    function quoteSendRegistrarListStatus(
        uint256 destinationChainId_,
        bytes32 listName_,
        address account_
    ) external view returns (uint256 fee_) {
        bool status_ = IRegistrarLike(registrar).listContains(listName_, account_);
        bytes memory payload_ = PayloadEncoder.encodeListUpdate(listName_, account_, status_);
        return IBridge(bridge).quote(destinationChainId_, payloadGasLimit[destinationChainId_][PayloadType.List], payload_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                     EXTERNAL INTERACTIVE FUNCTIONS                    //
    ///////////////////////////////////////////////////////////////////////////

    /// @inheritdoc IHubPortal
    function sendMTokenIndex(uint256 destinationChainId_, address refundAddress_) external payable returns (bytes32 messageId_) {
        _revertIfZeroRefundAddress(refundAddress_);

        uint128 index_ = _currentIndex();
        bytes memory payload_ = PayloadEncoder.encodeIndex(index_);

        messageId_ = _sendMessage(destinationChainId_, PayloadType.Index, refundAddress_, payload_);

        emit MTokenIndexSent(destinationChainId_, messageId_, index_);
    }

    /// @inheritdoc IHubPortal
    function sendRegistrarKey(
        uint256 destinationChainId_,
        bytes32 key_,
        address refundAddress_
    ) external payable returns (bytes32 messageId_) {
        _revertIfZeroRefundAddress(refundAddress_);

        bytes32 value_ = IRegistrarLike(registrar).get(key_);
        bytes memory payload_ = PayloadEncoder.encodeKey(key_, value_);

        messageId_ = _sendMessage(destinationChainId_, PayloadType.Key, refundAddress_, payload_);

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

    /// @inheritdoc IHubPortal
    function sendRegistrarListStatus(
        uint256 destinationChainId_,
        bytes32 listName_,
        address account_,
        address refundAddress_
    ) external payable returns (bytes32 messageId_) {
        _revertIfZeroRefundAddress(refundAddress_);

        bool status_ = IRegistrarLike(registrar).listContains(listName_, account_);
        bytes memory payload_ = PayloadEncoder.encodeListUpdate(listName_, account_, status_);

        messageId_ = _sendMessage(destinationChainId_, PayloadType.List, refundAddress_, payload_);

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

    /// @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_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                     OWNER INTERACTIVE FUNCTIONS                       //
    ///////////////////////////////////////////////////////////////////////////

    /// @inheritdoc IHubPortal
    function enableCrossSpokeConnection(uint256 spokeChainId_) external onlyOwner {
        if (crossSpokeConnectionEnabled[spokeChainId_]) return;

        crossSpokeConnectionEnabled[spokeChainId_] = true;
        uint256 bridgedPrincipal_ = bridgedPrincipal[spokeChainId_];

        // NOTE: Reset bridged principal, as tracking it
        //       for connected Spokes isn't possible on-chain.
        bridgedPrincipal[spokeChainId_] = 0;

        emit CrossSpokeConnectionEnabled(spokeChainId_, bridgedPrincipal_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                INTERNAL/PRIVATE INTERACTIVE FUNCTIONS                 //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @dev   Updates principal amount bridged to the destination chain.
     * @param destinationChainId_ The EVM id of the destination chain.
     * @param amount_             The amount of M Token to transfer.
     */
    function _burnOrLock(uint256 destinationChainId_, uint256 amount_) internal override {
        // Only track bridged principal for isolated Spokes
        if (crossSpokeConnectionEnabled[destinationChainId_]) return;

        // Won't overflow since `getPrincipalAmountRoundedDown` returns uint112
        unchecked {
            bridgedPrincipal[destinationChainId_] += IndexingMath.getPrincipalAmountRoundedDown(uint240(amount_), _currentIndex());
        }
    }

    /**
     * @dev   Unlocks M tokens to `recipient_`.
     * @param sourceChainId_ The EVM id of the source chain.
     * @param recipient_     The account to unlock/transfer M tokens to.
     * @param amount_        The amount of M Token to unlock to the recipient.
     */
    function _mintOrUnlock(uint256 sourceChainId_, address recipient_, uint256 amount_, uint128) internal override {
        // Only track bridged principal for isolated Spokes
        if (!crossSpokeConnectionEnabled[sourceChainId_]) {
            _decreaseBridgedPrincipal(sourceChainId_, amount_);
        }

        if (recipient_ != address(this)) {
            IERC20(mToken).transfer(recipient_, amount_);
        }
    }

    /// @dev Decreases the principal amount bridged when receiving transfer from a Spoke chain.
    ///      Reverts when trying to unlock more than was bridged to the Spoke.
    function _decreaseBridgedPrincipal(uint256 spokeChainId_, uint256 amount_) private {
        uint256 totalBridgedPrincipal = bridgedPrincipal[spokeChainId_];
        uint256 principalAmount = IndexingMath.getPrincipalAmountRoundedDown(uint240(amount_), _currentIndex());

        // Prevents unlocking more than was bridged to the Spoke
        if (principalAmount > totalBridgedPrincipal) revert InsufficientBridgedBalance();

        unchecked {
            bridgedPrincipal[spokeChainId_] = totalBridgedPrincipal - principalAmount;
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    //                 INTERNAL/PRIVATE 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 == IndexingMath.EXP_SCALED_ONE;
    }
}
"
    },
    "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/common/src/libs/IndexingMath.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.20 <0.9.0;

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

/**
 * @title  Helper library for indexing math functions.
 * @author M^0 Labs
 */
library IndexingMath {
    /* ============ Variables ============ */

    /// @notice The scaling of indexes for exponent math.
    uint56 internal constant EXP_SCALED_ONE = 1e12;

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

    /// @notice Emitted when a division by zero occurs.
    error DivisionByZero();

    /* ============ Exposed Functions ============ */

    /**
     * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded down.
     * @dev    Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
     */
    function divide240By128Down(uint240 x, uint128 y) internal pure returns (uint112) {
        if (y == 0) revert DivisionByZero();

        unchecked {
            // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are
            //       only used for the purpose of principal/present amount calculations for continuous indexing, and
            //       so for an `x` to be large enough to overflow this, it would have to be a possible result of
            //       `multiply112By128Down` or `multiply112By128Up`, which would already satisfy
            //       `uint256(x) * EXP_SCALED_ONE < type(uint240).max`.
            return UIntMath.safe112((uint256(x) * EXP_SCALED_ONE) / y);
        }
    }

    /**
     * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded up.
     * @dev    Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
     */
    function divide240By128Up(uint240 x, uint128 y) internal pure returns (uint112) {
        if (y == 0) revert DivisionByZero();

        unchecked {
            // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are
            //       only used for the purpose of principal/present amount calculations for continuous indexing, and
            //       so for an `x` to be large enough to overflow this, it would have to be a possible result of
            //       `multiply112By128Down` or `multiply112By128Up`, which would already satisfy
            //       `uint256(x) * EXP_SCALED_ONE < type(uint240).max`.
            return UIntMath.safe112(((uint256(x) * EXP_SCALED_ONE) + y - 1) / y);
        }
    }

    /**
     * @notice Helper function to calculate `(x * y) / EXP_SCALED_ONE`, rounded down.
     * @dev    Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
     */
    function multiply112By128Down(uint112 x, uint128 y) internal pure returns (uint240) {
        unchecked {
            return uint240((uint256(x) * y) / EXP_SCALED_ONE);
        }
    }

    /**
     * @notice Helper function to calculate `(x * index) / EXP_SCALED_ONE`, rounded up.
     * @dev    Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
     */
    function multiply112By128Up(uint112 x, uint128 index) internal pure returns (uint240 z) {
        unchecked {
            return uint240(((uint256(x) * index) + (EXP_SCALED_ONE - 1)) / EXP_SCALED_ONE);
        }
    }

    /**
     * @dev    Returns the present amount (rounded down) given the principal amount and an index.
     * @param  principalAmount The principal amount.
     * @param  index           An index.
     * @return The present amount rounded down.
     */
    function getPresentAmountRoundedDown(uint112 principalAmount, uint128 index) internal pure returns (uint240) {
        return multiply112By128Down(principalAmount, index);
    }

    /**
     * @dev    Returns the present amount (rounded up) given the principal amount and an index.
     * @param  principalAmount The principal amount.
     * @param  index           An index.
     * @return The present amount rounded up.
     */
    function getPresentAmountRoundedUp(uint112 principalAmount, uint128 index) internal pure returns (uint240) {
        return multiply112By128Up(principalAmount, index);
    }

    /**
     * @dev    Returns the principal amount given the present amount, using the current index.
     * @param  presentAmount The present amount.
     * @param  index         An index.
     * @return The principal amount rounded down.
     */
    function getPrincipalAmountRoundedDown(uint240 presentAmount, uint128 index) internal pure returns (uint112) {
        return divide240By128Down(presentAmount, index);
    }

    /**
     * @dev    Returns the principal amount given the present amount, using the current index.
     * @param  presentAmount The present amount.
     * @param  index         An index.
     * @return The principal amount rounded up.
     */
    function getPrincipalAmountRoundedUp(uint240 presentAmount, uint128 index) internal pure returns (uint112) {
        return divide240By128Up(presentAmount, index);
    }
}
"
    },
    "src/interfaces/IBridge.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

interface IBridge {
    ///////////////////////////////////////////////////////////////////////////
    //                             CUSTOM ERRORS                             //
    ///////////////////////////////////////////////////////////////////////////

    /// @notice Thrown when `sendMessage` function caller is not the portal.
    error NotPortal();

    /// @notice Thrown when the portal address is 0x0.
    error ZeroPortal();

    ///////////////////////////////////////////////////////////////////////////
    //                          VIEW/PURE FUNCTIONS                          //
    ///////////////////////////////////////////////////////////////////////////

    /// @notice Returns the address of the portal.
    function portal() external view returns (address);

    /**
     * @notice Returns the fee for sending a message to the remote chain
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  gasLimit           The gas limit to execute the message on the destination chain.
     * @param  payload            The message payload to send.
     * @return fee                The fee for sending a message.
     */
    function quote(uint256 destinationChainId, uint256 gasLimit, bytes memory payload) external view returns (uint256 fee);

    ///////////////////////////////////////////////////////////////////////////
    //                         INTERACTIVE FUNCTIONS                         //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @notice Sends a message to the remote chain.
     * @dev    Only EVM chains are supported.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  gasLimit           The gas limit to execute the message on the destination chain.
     * @param  refundAddress      The address to refund the fee to.
     * @param  payload            The message payload to send.
     * @return messageId          The unique identifier of the message sent.
     */
    function sendMessage(
        uint256 destinationChainId,
        uint256 gasLimit,
        address refundAddress,
        bytes memory payload
    ) external payable returns (bytes32 messageId);
}
"
    },
    "src/interfaces/IMTokenLike.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

/**
 * @title IMTokenLike interface
 * @author M^0 Labs
 * @notice  Subset of M Token interface required for Portal contracts.
 */
interface IMTokenLike {
    /**
     * @notice Emitted when there is insufficient balance to decrement from `account`.
     * @param  account     The account with insufficient balance.
     * @param  rawBalance  The raw balance of the account.
     * @param  amount      The amount to decrement the `rawBalance` by.
     */
    error InsufficientBalance(address account, uint256 rawBalance, uint256 amount);

    /// @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  IRegistrarLike interface
 * @author M^0 Labs
 * @notice Subset of Registrar interface required for Portal contracts.
 */
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/IPortal.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

import { PayloadType } from "../libs/PayloadEncoder.sol";

/**
 * @title  IPortal interface
 * @author M^0 Labs
 * @notice Subset of functions inherited by both IHubPortal and ISpokePortal.
 */
interface IPortal {
    ///////////////////////////////////////////////////////////////////////////
    //                                 EVENTS                                //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @notice Emitted when M token is sent to a destination chain.
     * @param  sourceToken        The address of the token on the source chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  destinationToken   The address of the token on the destination chain.
     * @param  sender             The address that bridged the M tokens via the Portal.
     * @param  recipient          The account receiving tokens on destination chain.
     * @param  amount             The amount of tokens.
     * @param  index              The M token index.
     * @param  messageId          The unique identifier for the sent message.
     */
    event MTokenSent(
        address indexed sourceToken,
        uint256 destinationChainId,
        address destinationToken,
        address indexed sender,
        address indexed recipient,
        uint256 amount,
        uint128 index,
        bytes32 messageId
    );

    /**
     * @notice Emitted when M token is received from a source chain.
     * @param  sourceChainId    The EVM chain Id of the source chain.
     * @param  destinationToken The address of the token on the destination chain.
     * @param  sender           The account sending tokens.
     * @param  recipient        The account receiving tokens.
     * @param  amount           The amount of tokens.
     * @param  index            The M token index
     */
    event MTokenReceived(
        uint256 sourceChainId,
        address indexed destinationToken,
        address 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 the Bridge contract responsible for cross-chain communication is set
     * @param  previousBridge The address of the previous Bridge.
     * @param  newBridge      The address of the new Bridge.
     */
    event BridgeSet(address indexed previousBridge, address indexed newBridge);

    /**
     * @notice Emitted when M token is set for the remote chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  mToken             The address of M token on the destination chain.
     */
    event DestinationMTokenSet(uint256 indexed destinationChainId, address mToken);

    /**
     * @notice Emitted when a bridging path support status is updated.
     * @param  sourceToken        The address of the token on the current chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  destinationToken   The address of the token on the destination chain.
     * @param  supported          `True` if the token is supported, `false` otherwise.
     */
    event SupportedBridgingPathSet(
        address indexed sourceToken, uint256 indexed destinationChainId, address indexed destinationToken, bool supported
    );

    /**
     * @notice Emitted when the gas limit for a payload type is updated.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  payloadType        The type of payload.
     * @param  gasLimit           The gas limit.
     */
    event PayloadGasLimitSet(uint256 indexed destinationChainId, PayloadType indexed payloadType, uint256 gasLimit);

    ///////////////////////////////////////////////////////////////////////////
    //                             CUSTOM ERRORS                             //
    ///////////////////////////////////////////////////////////////////////////

    /// @notice Thrown when the M token is 0x0.
    error ZeroMToken();

    /// @notice Thrown when the M token is 0x0.
    error ZeroRemoteMToken();

    /// @notice Thrown when the Registrar address is 0x0.
    error ZeroRegistrar();

    /// @notice Thrown when the Swap Facility address is 0x0.
    error ZeroSwapFacility();

    /// @notice Thrown when the Bridge address is 0x0.
    error ZeroBridge();

    /// @notice Thrown when the source token address is 0x0.
    error ZeroSourceToken();

    /// @notice Thrown when the destination token address is 0x0.
    error ZeroDestinationToken();

    /// @notice Thrown when the transfer amount is 0.
    error ZeroAmount();

    /// @notice Thrown when the refund address is 0x0.
    error ZeroRefundAddress();

    /// @notice Thrown when the recipient address is 0x0.
    error ZeroRecipient();

    /// @notice Thrown when `receiveMessage` function caller is not the bridge.
    error NotBridge();

    /// @notice Thrown in `transferMLikeToken` function when bridging path is not supported
    error UnsupportedBridgingPath(address sourceToken, uint256 destinationChainId, address destinationToken);

    /// @notice Thrown when the destination chain id is equal to the source one.
    error InvalidDestinationChain(uint256 destinationChainId);

    ///////////////////////////////////////////////////////////////////////////
    //                          VIEW/PURE FUNCTIONS                          //
    ///////////////////////////////////////////////////////////////////////////

    /// @notice The current index of the Portal's earning mechanism.
    function currentIndex() external view returns (uint128);

    /// @notice The address of the M token.
    function mToken() external view returns (address);

    /// @notice The address of the Registrar contract.
    function registrar() external view returns (address);

    /// @notice The address of the Bridge contract responsible for cross-chain communication.
    function bridge() external view returns (address);

    /// @notice The address of the Swap Facility contract.
    function swapFacility() external view returns (address);

    /// @notice The address of the original caller of `transfer` and `transferMLikeToken` functions.
    function msgSender() external view returns (address);

    /**
     * @notice Returns the address of M token on the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @return mToken             The address of M token on the destination chain.
     */
    function destinationMToken(uint256 destinationChainId) external view returns (address mToken);

    /**
     * @notice Indicates whether the provided bridging path is supported.
     * @param  sourceToken        The address of the token on the current chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  destinationToken   The address of the token on the destination chain.
     * @return supported          `True` if the token is supported, `false` otherwise.
     */
    function supportedBridgingPath(
        address sourceToken,
        uint256 destinationChainId,
        address destinationToken
    ) external view returns (bool supported);

    /**
     * @notice Returns the gas limit required to process a message
     *         with the specified payload type on the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  payloadType        The type of payload.
     * @return gasLimit           The gas limit.
     */
    function payloadGasLimit(uint256 destinationChainId, PayloadType payloadType) external view returns (uint256 gasLimit);

    /**
     * @notice Returns the delivery fee for token transfer.
     * @dev    The fee must be passed as mgs.value when calling `transfer` or `transferMLikeToken`.
     * @param  amount             The amount of tokens to transfer.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  recipient          The account to receive tokens.
     * @param  fee                The delivery fee.
     */
    function quoteTransfer(uint256 amount, uint256 destinationChainId, address recipient) external view returns (uint256 fee);

    ///////////////////////////////////////////////////////////////////////////
    //                         INTERACTIVE FUNCTIONS                         //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @notice Initializes the Proxy's storage
     * @param  bridge_        The address of the Bridge contract.
     * @param  initialOwner_  The address of the owner.
     * @param  initialPauser_ The address of the pauser.
     */
    function initialize(address bridge_, address initialOwner_, address initialPauser_) external;

    /**
     * @notice Sets address of the Bridge contract responsible for cross-chain communication.
     * @param  bridge The address of the Bridge.
     */
    function setBridge(address bridge) external;

    /**
     * @notice Sets M token address on the remote chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  mToken             The address of M token on the destination chain.
     */
    function setDestinationMToken(uint256 destinationChainId, address mToken) external;

    /**
     * @notice Sets a bridging path support status.
     * @param  sourceToken        The address of the token on the current chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  destinationToken   The address of the token on the destination chain.
     * @param  supported          `True` if the token is supported, `false` otherwise.
     */
    function setSupportedBridgingPath(
        address sourceToken,
        uint256 destinationChainId,
        address destinationToken,
        bool supported
    ) external;

    /**
     * @notice Sets the gas limit required to process a message
     *         with the specified payload type on the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  payloadType        The payload type.
     * @param  gasLimit           The gas limit required to process the message.
     */
    function setPayloadGasLimit(uint256 destinationChainId, PayloadType payloadType, uint256 gasLimit) external;

    /**
     * @notice Transfers M token to the destination chain.
     * @param  amount             The amount of tokens to transfer.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  recipient          The account to receive tokens.
     * @param  refundAddress      The address to receive excess native gas on the source chain.
     * @return messageId          The unique identifier of the message sent.
     */
    function transfer(
        uint256 amount,
        uint256 destinationChainId,
        address recipient,
        address refundAddress
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Transfers M or Wrapped M Token to the destination chain.
     * @dev    If wrapping on the destination fails, the recipient will receive $M token.
     * @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 EVM chain Id of the destination chain.
     * @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 source chain.
     * @return messageId          The unique identifier of the message sent.
     */
    function transferMLikeToken(
        uint256 amount,
        address sourceToken,
        uint256 destinationChainId,
        address destinationToken,
        address recipient,
        address refundAddress
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Receives a message from the bridge.
     * @param  sourceChainId The EVM chain Id of the source chain.
     * @param  payload       The message payload.
     */
    function receiveMessage(uint256 sourceChainId, bytes calldata payload) external;
}
"
    },
    "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 cross-Spoke connection is enabled for the Spoke chain.
     * @param  spokeChainId     The EVM chain Id of the Spoke.
     * @param  bridgedPrincipal The principal amount of M tokens bridged to the Spoke chain before the connection was enabled.
     */
    event CrossSpokeConnectionEnabled(uint256 spokeChainId, uint256 bridgedPrincipal);

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

    /**
     * @notice Emitted when the Registrar key is sent to a destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @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(uint256 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 EVM chain Id of the destination chain.
     * @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(
        uint256 destinationChainId, bytes32 messageId, bytes32 indexed listName, address indexed account, bool status
    );

    ///////////////////////////////////////////////////////////////////////////
    //                             CUSTOM ERRORS                             //
    ///////////////////////////////////////////////////////////////////////////

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

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

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

    /// @notice Thrown when trying to unlock more tokens than was locked.
    error InsufficientBridgedBalance();

    ///////////////////////////////////////////////////////////////////////////
    //                          VIEW/PURE FUNCTIONS                          //
    ///////////////////////////////////////////////////////////////////////////

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

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

    /// @notice Returns the principal amount of M tokens bridged to a specified Spoke chain.
    /// @dev    Only applicable to isolated Spokes (i.e., `crossSpokeConnectionEnabled` == false).
    function bridgedPrincipal(uint256 spokeChainId) external view returns (uint256 principal);

    /// @notice Indicates whether a given Spoke chain can communicate with other Spokes.
    function crossSpokeConnectionEnabled(uint256 spokeChainId) external view returns (bool enabled);

    /**
     * @notice Returns the delivery fee for sending $M token index.
     * @dev    The fee must be passed as mgs.value when calling `sendMTokenIndex`.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  fee                The delivery fee.
     */
    function quoteSendIndex(uint256 destinationChainId) external view returns (uint256 fee);

    /**
     * @notice Returns the delivery fee for sending Registrar key and value.
     * @dev    The fee must be passed as mgs.value when calling `sendRegistrarKey`.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  key                The Registrar key to send.
     * @param  fee                The delivery fee.
     */
    function quoteSendRegistrarKey(uint256 destinationChainId, bytes32 key) external view returns (uint256 fee);

    /**
     * @notice Returns the delivery fee for sending Registrar list status.
     * @dev    The fee must be passed as mgs.value when calling `sendRegistrarListStatus`.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  listName           The name of the list.
     * @param  account            The account.
     * @param  fee                The delivery fee.
     */
    function quoteSendRegistrarListStatus(
        uint256 destinationChainId,
        bytes32 listName,
        address account
    ) external view returns (uint256 fee);

    ///////////////////////////////////////////////////////////////////////////
    //                         INTERACTIVE FUNCTIONS                         //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @notice Sends the $M token index to the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  refundAddress      The refund address to receive excess native gas.
     * @return messageId          The ID uniquely identifying the message.
     */
    function sendMTokenIndex(uint256 destinationChainId, address refundAddress) external payable returns (bytes32 messageId);

    /**
     * @notice Sends the Registrar key to the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  key                The key to send.
     * @param  refundAddress      The refund address to receive excess native gas.
     * @return messageId          The ID uniquely identifying the message
     */
    function sendRegistrarKey(
        uint256 destinationChainId,
        bytes32 key,
        address refundAddress
    ) external payable returns (bytes32 messageId);

    /**
     * @notice Sends the Registrar list status for an account to the destination chain.
     * @param  destinationChainId The EVM chain Id of the destination chain.
     * @param  listName           The name of the list.
     * @param  account            The account.
     * @param  refundAddress      The refund address to receive excess native gas.
     * @return messageId          The ID uniquely identifying the message.
     */
    function sendRegistrarListStatus(
        uint256 destinationChainId,
        bytes32 listName,
        address account,
        address refundAddress
    ) external payable returns (bytes32 messageId);

    /// @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;

    /**
     * @notice Enables cross-Spoke connection for the specified Spoke chain.
     * @param  spokeChainId The EVM chain Id of the Spoke chain.
     */
    function enableCrossSpokeConnection(uint256 spokeChainId) external;
}
"
    },
    "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 { Migratable } from "../lib/common/src/Migratable.sol";
import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol";
import { ReentrancyLock } from "../lib/uniswap-v4-periphery/src/base/ReentrancyLock.sol";

import { IPortal } from "./interfaces/IPortal.sol";
import { IBridge } from "./interfaces/IBridge.sol";
import { ISwapFacilityLike } from "./interfaces/ISwapFacilityLike.sol";
import { PausableOwnableUpgradeable } from "./access/PausableOwnableUpgradeable.sol";
import { TypeConverter } from "./libs/TypeConverter.sol";
import { SafeCall } from "./libs/SafeCall.sol";
import { PayloadType, PayloadEncoder } from "./libs/PayloadEncoder.sol";

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

    /// @inheritdoc IPortal
    address public immutable mToken;

    /// @inheritdoc IPortal
    address public immutable registrar;

    /// @inheritdoc IPortal
    address public immutable swapFacility;

    /// @inheritdoc IPortal
    address public bridge;

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

    /// @inheritdoc IPortal
    mapping(uint256 destinationChainId => address mToken) public destinationMToken;

    /// @inheritdoc IPortal
    mapping(uint256 destinationChainId => mapping(PayloadType payloadType => uint256 gasLimit)) public payloadGasLimit;

    /**
     * @notice Constructs the Implementation contract
     * @dev    Sets immutable storage.
     * @param  mToken_    The address of M token.
     * @param  registrar_ The address of Registrar.
     * @param  swapFacility_ The address of Swap Facility.
     */
    constructor(address mToken_, address registrar_, address swapFacility_) {
        _disableInitializers();

        if ((mToken = mToken_) == address(0)) revert ZeroMToken();
        if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
        if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility();
    }

    /**
     * @notice Initializes the Proxy's storage
     * @param  bridge_        The address of M token.
     * @param  initialOwner_  The address of the owner.
     * @param  initialPauser_ The address of the pauser.
     */
    function _initialize(address bridge_, address initialOwner_, address initialPauser_) internal onlyInitializing {
        if ((bridge = bridge_) == address(0)) revert ZeroBridge();
        __PausableOwnable_init(initialOwner_, initialPauser_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                     EXTERNAL VIEW/PURE FUNCTIONS                      //
    ///////////////////////////////////////////////////////////////////////////

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

    /// @inheritdoc IPortal
    function quoteTransfer(
        uint256 amount_,
        uint256 destinationChainId_,
        address recipient_
    ) external view returns (uint256 fee_) {
        // NOTE: for quoting delivery only the payload size and destination chain matter.
        address destinationToken_ = destinationMToken[destinationChainId_];
        bytes memory payload_ =
            PayloadEncoder.encodeTokenTransfer(amount_, destinationToken_, msg.sender, recipient_, _currentIndex());
        return IBridge(bridge).quote(destinationChainId_, payloadGasLimit[destinationChainId_][PayloadType.Token], payload_);
    }

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

    ///////////////////////////////////////////////////////////////////////////
    //                     EXTERNAL INTERACTIVE FUNCTIONS                    //
    ///////////////////////////////////////////////////////////////////////////

    /// @inheritdoc IPortal
    function transfer(
        uint256 amount_,
        uint256 destinationChainId_,
        address recipient_,
        address refundAddress_
    ) external payable whenNotPaused isNotLocked returns (bytes32 messageId_) {
        return _transferMLikeToken(
            amount_, mToken, destinationChainId_, destinationMToken[destinationChainId_], recipient_, refundAddress_
        );
    }

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

        return _transferMLikeToken(amount_, sourceToken_, destinationChainId_, destinationToken_, recipient_, refundAddress_);
    }

    /// @inheritdoc IPortal
    function receiveMessage(uint256 sourceChainId_, bytes calldata payload_) external {
        if (msg.sender != bridge) revert NotBridge();

        PayloadType payloadType_ = payload_.getPayloadType();

        if (payloadType_ == PayloadType.Token) {
            _receiveMLikeToken(sourceChainId_, payload_);
            return;
        }

        _receiveCustomPayload(payloadType_, payload_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                     OWNER INTERACTIVE FUNCTIONS                       //
    ///////////////////////////////////////////////////////////////////////////

    /// @inheritdoc IPortal
    function setBridge(address newBridge_) external onlyOwner {
        if (newBridge_ == address(0)) revert ZeroBridge();
        address previousBridge_ = bridge;

        bridge = newBridge_;
        emit BridgeSet(previousBridge_, newBridge_);
    }

    /// @inheritdoc IPortal
    function setDestinationMToken(uint256 destinationChainId_, address mToken_) external onlyOwner {
        if (destinationChainId_ == block.chainid) revert InvalidDestinationChain(destinationChainId_);
        if (mToken_ == address(0)) revert ZeroMToken();

        destinationMToken[destinationChainId_] = mToken_;
        emit DestinationMTokenSet(destinationChainId_, mToken_);
    }

    /// @inheritdoc IPortal
    function setSupportedBridgingPath(
        address sourceToken_,
        uint256 destinationChainId_,
        address destinationToken_,
        bool supported_
    ) external onlyOwner {
        if (sourceToken_ == address(0)) revert ZeroSourceToken();
        if (destinationChainId_ == block.chainid) revert InvalidDestinationChain(destinationChainId_);
        if (destinationToken_ == address(0)) revert ZeroDestinationToken();

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

    /// @inheritdoc IPortal
    function setPayloadGasLimit(uint256 destinationChainId_, PayloadType payloadType_, uint256 gasLimit_) external onlyOwner {
        payloadGasLimit[destinationChainId_][payloadType_] = gasLimit_;
        emit PayloadGasLimitSet(destinationChainId_, payloadType_, gasLimit_);
    }

    /**
     * @dev   Performs the contract migration by delegate-calling `migrator_`.
     * @param migrator_ The address of a migrator contract.
     */
    function migrate(address migrator_) external onlyOwner {
        _migrate(migrator_);
    }

    ///////////////////////////////////////////////////////////////////////////
    //                INTERNAL/PRIVATE INTERACTIVE FUNCTIONS                 //
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @dev Transfers M or Wrapped M token to the remote chain.
     * @param  amount_             The amount of tokens to transfer.
     * @param  sourceToken_        The address of the source token.
     * @param  destinationChainId_ The EVM chain Id of the destination chain.
     * @param  destinationToken_   The address of the destination token.
     * @param  recipient_          The address of the recipient.
     * @param  refundAddress_      The address to receive the fee refund.
     * @return messageId_          The ID uniquely identifying the message.
     */
    function _transferMLikeToken(
        uint256 amount_,
        address sourceToken_,
        uint256 destinationChainId_,
        address destinationToken_,
        address recipient_,
        address refundAddress_
    ) private returns (bytes32 messageId_) {
        _revertIfZeroAmount(amount_);
        _revertIfZeroRefundAddress(refundAddress_);

        if (destinationToken_ == address(0)) revert ZeroDestinationToken();
        if (recipient_ == address(0)) revert ZeroRecipient();

        IERC20 mToken_ = IERC20(mToken);
        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_;
                    // Ensure that updated transfer amount is greater than 0
                    _revertIfZeroAmount(amount_);
                }
            }
        }

        // Burn M tokens on Spoke.
        // In case of Hub, only update the bridged principal amount as tokens already transferred.
        _burnOrLock(destinationChainId_, amount_);

        uint128 index_ = _currentIndex();
        bytes memory payload_ = PayloadEncoder.encodeTokenTransfer(amount_, destinationToken_, msg.sender, recipient_, index_);
        messageId_ = _sendMessage(destinationChainId_, PayloadType.Token, refundAddress_, payload_);

        // Prevent stack too deep
        uint256 transferAmount_ = amount_;

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

    /**
     * @dev   Sends a cross-chain message using the bridge.
     * @param destinationChainId_ The EVM chain Id of the destination chain.
     * @param payloadType_        The type of the payload.
     * @param refundAddress_      The address to receive the fee refund.
     * @param payload_            The message payload to send.
     * @return messageId_         The ID uniquely identifying the message.
     */
    function _sendMessage(
        uint256 destinationChainId_,
        PayloadType payloadType_,
        address refundAddress_,
        bytes memory payload_
    ) internal returns (bytes32 messageId_) {
        return IBridge(bridge).sendMessage{ value: msg.value }(
            destinationChainId_, payloadGasLimit[destinationChainId_][payloadType_], refundAddress_, payload_
        );
    }

    /**
     * @dev   Handles token transfer message on the destination.
     * @param sourceChainId_ The EVM chain Id of the source chain.
     * @param payload_       The message payload.
     */
    function _receiveMLikeToken(uint256 sourceChainId_, bytes memory payload_) private {
        (uint256 amount_, address destinationToken_, address sender_, address recipient_, uint128 index_) =
            payload_.decodeTokenTransfer();

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

        address mToken_ = mToken;
        if (destinationToken_ == mToken_) {
            // mints or unlocks M Token to the recipient
            _mintOrUnlock(sourceChainId_, recipient_, amount_, index_);
        } else {
            // mints or unlocks M Token to the Portal
            _mintOrUnlock(sourceChainId_, address(this), amount_, index_);

            // wraps M token and transfers it to the recipient
            _wrap(mToken_, destinationToken_, recipient_, amount_);
        }
    }

    /**
     * @dev   Wraps $M token to the token specified by `destinationWrappedToken_`.
     *        If wrapping fails transfers $M token to `recipient_`.
     * @param mToken_                  The address of $M token.
     * @param destinationWrappedToken_ The address of the wrapped token.
     * @param recipient_               The account to receive wrapped token.
     * @param amount_                  The amount to wrap.
     */
    function _wrap(address mToken_, address destinationWrappedToken_, address recipient_, uint256 amount_) private {
        IERC20(mToken_).approve(swapFacility, amount_);

        // Attempt to wrap $M token
        // NOTE: the call might fail with out-of-gas exception
        //       even if the destination token is the valid wrapped M token.
        //       Recipients must support both $M and wrapped $M transfers.
        (bool success,) =
            swapFacility.call(abi.encodeCall(ISwapFacilityLike.swapInM, (destinationWrappedToken_, amount_, recipient_)));

        if (!success) {
            emit WrapFailed(destinationWrappedToken_, recipient_, amount_);
            // Reset approval to prevent a potential double-spend attack
            IERC20(mToken_).approve(swapFacility, 0);
            // Transfer $M token to the recipient
            IERC20(mToken_).transfer(recipient_, amount_);
        }
    }

    /**
     * @dev   Overridden in SpokePortal to handle custom payload messages.
     * @param payloadType_  The type of the payload (Index, Key, or List).
     * @param payload_      The message payload to process.
     */
    function _receiveCustomPayload(PayloadType payloadType_, bytes memory payload_) internal virtual { }

    /**
     * @dev   HubPortal:   unlocks and transfers `amount_` M tokens to `recipient_`.
     *        SpokePortal: mints `amount_` M tokens to `recipient_`.
     * @param sourceChainId_ The EVM id of the source chain.
     * @param recipient_     The account receiving M tokens.
     * @param amount_        The amount of M tokens to unlock/mint.
     * @param index_         The index from the source chain.
     */
    function _mintOrUnlock(uint256 sourceChainId_, address recipient_, uint256 amount_, uint128 index_) internal virtual { }

    /**
     * @dev   HubPortal:   locks amount_` M tokens.
     *        SpokePortal: burns `amount_` M tokens.
     * @param destinationChainId_ The EVM id of the destination chain.
     * @param amount_             The amount of M tokens to lock/burn.
     */
    function _burnOrLock(uint256 destinationChainId_, uint256 amount_) internal virtual { }

    ///////////////////////////////////////////////////////////////////////////
    //                 INTERNAL/PRIVATE VIEW/PURE FUNCTIONS                  //
    ///////////////////////////////////////////////////////////////////////////

    /// @dev Reverts if `amount` is zero.
    function _revertIfZeroAmount(uint256 amount_) private pure {
        if (amount_ == 0) revert ZeroAmount();
    }

    /// @dev Reverts if `refundAddress` is zero address.
    function _revertIfZeroRefundAddress(address refundAddress_) internal pure {
        if (refundAddress_ == address(0)) revert ZeroRefundAddress();
    }

    /// @inheritdoc Migratable
    function _getMigrator() internal pure override returns (address migrator_) {
        // NOTE: in this version only the owner-controlled migration via `migrate()` function is supported
        return address(0);
    }

    /// @dev Returns the current M token index used by the Portal.
    function _currentIndex() internal view virtual returns (uint128) { }

    /// @dev Returns the maximum rounding error that can occur when transferring M tokens to the Portal
    function _getMaxRoundingError() private view returns (uint256) {
        return _currentIndex() / IndexingMath.EXP_SCALED_ONE + 1;
    }
}
"
    },
    "src/libs/PayloadEncoder.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.26;

import { BytesParser } from "./BytesParser.sol";
import { TypeConverter } from "./TypeConverter.sol";

enum PayloadType {
    Token,
    Index,
    Key,
    List
}

/**
 * @title  PayloadEncoder
 * @author M^0 Labs
 * @notice Encodes and decodes cross-chain message payloads.
 */
library PayloadEncoder {
    using BytesParser for bytes;
    using TypeConverter for *;

    uint256 internal constant PAYLOAD_TYPE_LENGTH = 1;

    /// @dev PayloadType.Token = 0, PayloadType.Index = 1, PayloadType.Key = 2, PayloadType.List = 3
    uint256 internal constant MAX_PAYLOAD_TYPE = 3;

    error InvalidPayloadLength(uint256 length);
    error InvalidPayloadType(uint

Tags:
ERC20, Multisig, Pausable, Swap, Yield, Upgradeable, Multi-Signature, Factory|addr:0xff6954d6109b14b63fb5268daac09647305f954c|verified:true|block:23723655|tx:0xc8421b00e913f0b74a20ee24df4144622cf87a72618a6328a2ff813cc3c6186d|first_check:1762253041

Submitted on: 2025-11-04 11:44:04

Comments

Log in to comment.

No comments yet.