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
Submitted on: 2025-11-04 11:44:04
Comments
Log in to comment.
No comments yet.