MetaERC20Spoke

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/token/MetaERC20Spoke.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.26;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";

import {FinalityState} from "../lib/MetalayerMessage.sol";
import {MetaERC20Message} from "../lib/MetaERC20Message.sol";
import {MetaERC20MessageStruct, MetaERC20MessageType} from "../lib/MetaERC20Types.sol";
import {ReadOperation} from "../interfaces/IMetalayerRecipient.sol";

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

/// @title MetaERC20Spoke
/// @notice Synthetic bridge-side contract for minting and burning cross-chain ERC20 tokens
/// @dev Mints synthetic tokens on receipt of MintRequest, burns tokens during transferRemote.
///      Uses ERC20's built-in balance tracking for mint/burn operations.
///      Implements smart routing: direct transfers to other Spokes, or via Hub for high-value transfers.
///
/// Spoke behavior:
/// - Mints synthetic tokens when receiving MintRequest messages
/// - Burns synthetic tokens before dispatching transferRemote
/// - Routes to Hub (UnlockRequest) when users want canonical tokens
/// - Routes high-value transfers through Hub (SecurityRelay) for manual approval
/// - Routes normal transfers directly to destination Spokes (MintRequest)
/// - Converts between source and local token decimal precision
/// - Uses ERC20 balance validation to prevent over-burning
///
/// Must be paired with a MetaERC20Hub on the canonical domain.
///
contract MetaERC20Spoke is MetaERC20Base, ERC20Upgradeable {
    using TypeCasts for address;
    using TypeCasts for bytes32;

    /*//////////////////////////////////////////////////////////////
                            VARIABLES
    //////////////////////////////////////////////////////////////*/

    /*//////////////////////////////////////////////////////////////
                        STORAGE LAYOUT (SLOT MAP)
    ----------------------------------------------------------------
    |        | MetaERC20Base           |        |                  |
    |--------|-------------------------|--------|------------------|
    | 67     | __reservedSlot67        | 32 B   | Reserved         |
    | 68     | __reservedSlot63        | 28 B   | Spoke-specific   |
    |        | hubDomain               | 4 B    | Spoke-specific   |
    | 69     | securityThreshold       | 32 B   |                  |
    | 70–120 | __gap (Spoke)           | 50x32B | Reserved (Spoke) |
    ----------------------------------------------------------------
    Total declared slots: 121
    //////////////////////////////////////////////////////////////*/

    /// @dev Reserved padding - slot 67 previously used for mintedBalance, now available
    uint256 private __reservedSlot67;

    /// @dev Reserved padding to align `hubDomain` at the low bytes of slot 63
    uint224 private __reservedSlot63;

    /// @notice The Metalayer domain ID of the canonical Hub contract
    /// @dev Used to route messages and validate origin in `_validateOrigin`
    uint32 public hubDomain;

    /// @notice Transfers >= securityThreshold must route through the Hub for manual approval
    /// @dev Threshold is in local token units. High-value transfers use SecurityRelay for additional security.
    uint256 public securityThreshold;
    /*//////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    /// @notice Thrown when the configured hubDomain is zero or otherwise invalid
    error InvalidHubDomain();

    /*//////////////////////////////////////////////////////////////
                            EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when an admin manually reissues synthetic tokens using EMERGENCY_REISSUE
    /// @param transferId The ID of the transfer that was force-reissued
    /// @param adminAddress The address of the admin who triggered the reissue
    /// @param recipientAddress The address that received the reissued tokens
    event MetaERC20AdminReissue(
        bytes32 indexed transferId,
        address indexed adminAddress,
        address recipientAddress
    );

    /// @notice Emitted when the MetaERC20Spoke is initialized
    /// @param localDomain The Metalayer domain ID of this Spoke chain
    /// @param hubDomain The Metalayer domain ID of the canonical Hub
    /// @param tokenDecimals The number of decimals the synthetic token uses
    /// @param metaERC20Version The protocol version used by this Spoke
    /// @param securityThreshold Transfers >= this value are routed through the Hub (in local token units)
    /// @param name The ERC20 name string
    /// @param symbol The ERC20 symbol string
    event MetaERC20SpokeInitialized(
        uint32 indexed localDomain,
        uint32 indexed hubDomain,
        uint8 tokenDecimals,
        uint8 metaERC20Version,
        uint256 securityThreshold,
        string name,
        string symbol
    );

    /// @notice Emitted when the security threshold is updated by an admin
    /// @param newThreshold The new threshold value for high-value transfer routing (in local token units)
    event SecurityThresholdUpdated(uint256 newThreshold);

    /// @notice Emitted when the hub domain is updated
    /// @param newHubDomain The new hub domain ID
    event HubDomainUpdated(uint32 newHubDomain);

    /*//////////////////////////////////////////////////////////////
                            INITIALIZER
    //////////////////////////////////////////////////////////////*/

    /// @notice Initializes the MetaERC20Spoke with required configuration
    /// @dev Callable only once. Initializes ERC20 metadata, router config, and decimal conversion settings.
    ///      Grants ADMIN_ROLE to the initializer. Sets up 1-day delay for admin role transfers.
    ///      Validates hubDomain is non-zero and different from localDomain.
    /// @param _localDomain The Metalayer domain ID of the current chain
    /// @param _metalayerRouter The address of the MetalayerRouter used for message dispatch and receive
    /// @param _ttlWindow The time (in seconds) after which transfer records can be pruned
    /// @param _metaERC20Version The MetaERC20 protocol version to use for outbound messages
    /// @param _hubDomain The Metalayer domain ID of the canonical Hub
    /// @param name_ The name of the synthetic ERC20 token
    /// @param symbol_ The symbol of the synthetic ERC20 token
    /// @param _tokenDecimals The number of decimals the token uses
    /// @param _securityThreshold The threshold for high-value transfers (in local token units)
    function initialize(
        uint32 _localDomain,
        address _metalayerRouter,
        uint256 _ttlWindow,
        uint8 _metaERC20Version,
        uint32 _hubDomain,
        string memory name_,
        string memory symbol_,
        uint8 _tokenDecimals,
        uint256 _securityThreshold,
        address _initialAdmin
    ) public initializer {
        __ERC20_init(name_, symbol_);
        __AccessControlDefaultAdminRules_init(1 days, _initialAdmin);

        if (_hubDomain == 0 || _hubDomain == _localDomain)
            revert InvalidHubDomain();

        _initializeBase(
            _localDomain,
            _metalayerRouter,
            _metaERC20Version,
            _ttlWindow,
            _tokenDecimals
        );

        hubDomain = _hubDomain;
        securityThreshold = _securityThreshold;
        isHub = false;

        _grantRole(ADMIN_ROLE, _initialAdmin);

        emit MetaERC20SpokeInitialized(
            _localDomain,
            _hubDomain,
            _tokenDecimals,
            _metaERC20Version,
            _securityThreshold,
            name_,
            symbol_
        );
    }

    /*//////////////////////////////////////////////////////////////
                        OUTGOING FUNCTIONS (external)
    //////////////////////////////////////////////////////////////*/

    /// @inheritdoc MetaERC20Base
    function transferRemote(
        uint32 _recipientDomain,
        bytes32 _recipientAddress,
        uint256 _amount
    ) public payable override returns (bytes32 transferId) {
        return
            super.transferRemote(
                _recipientDomain,
                _recipientAddress,
                _amount
            );
    }

    /*//////////////////////////////////////////////////////////////
                        INCOMING FUNCTIONS (external)
    //////////////////////////////////////////////////////////////*/

    /// @inheritdoc MetaERC20Base
    function handle(
        uint32 _originDomain,
        bytes32 _senderAddress,
        bytes calldata _message,
        ReadOperation[] calldata _reads,
        bytes[] calldata _results
    ) public payable override {
        super.handle(_originDomain, _senderAddress, _message, _reads, _results);
    }

    /*//////////////////////////////////////////////////////////////
                        INCOMING FUNCTIONS (internal)
    //////////////////////////////////////////////////////////////*/

    /// @notice Handles a MintRequest to mint synthetic tokens to a user
    /// @dev Decodes MetaERC20MessageStruct, converts decimal precision from source to local units,
    ///      and mints synthetic tokens to the recipient.
    ///      Uses CEI pattern: validates recipient, converts decimals, checks amount, then mints.
    /// @param _originDomain The Metalayer domain ID where the message originated
    /// @param _senderAddress The address that sent the message on the origin chain (bytes32 format)
    /// @param _metaERC20Version The version of the MetaERC20 protocol
    /// @param _writeData The raw encoded message payload (already validated and version-matched)
    function _handleMintRequest(
        uint32 _originDomain,
        bytes32 _senderAddress,
        uint8 _metaERC20Version,
        bytes calldata _writeData
    ) internal override {
        MetaERC20MessageStruct memory _message = MetaERC20Message.decodeMessage(
            _writeData
        );

        if (_message.recipient == address(0)) revert InvalidRecipient();

        uint256 localAmount = _convertDecimals(
            _message.amount,
            _message.sourceDecimals,
            tokenDecimals
        );
        if (localAmount == 0) revert ZeroAmount();

        _mint(_message.recipient, localAmount);

        emit MetaERC20Received(
            _message.transferId,
            _originDomain,
            _senderAddress,
            localDomain,
            _message.recipient.addressToBytes32(),
            _message.amount,
            _message.sourceDecimals,
            _message.messageType,
            _metaERC20Version,
            metaERC20Addresses[_originDomain], // sourceTokenAddress
            address(this).addressToBytes32() // destinationTokenAddress (Spoke contract itself)
        );
    }

    /*//////////////////////////////////////////////////////////////
                        ERC20 FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Returns the number of decimals used by the synthetic token
    /// @dev Overrides ERC20 to return the value configured at initialization.
    ///      Critical for proper external contract integration and display.
    /// @return The number of decimals for this token
    function decimals() public view virtual override returns (uint8) {
        return tokenDecimals;
    }
    /*//////////////////////////////////////////////////////////////
                        ADMIN FUNCTIONS (external)
    //////////////////////////////////////////////////////////////*/

    /// @notice Admin-only emergency function to reissue synthetic tokens for a failed or unprocessed transfer
    /// @dev Bypasses normal message flow and directly mints synthetic tokens to recipient.
    ///      Converts amounts from source decimals to local token decimals before minting.
    ///      Emits MetaERC20Received with originDomain = 0 to signal admin override flow.
    /// @param _transferId The ID of the original transfer to reissue from storage
    /// @param _recipientAddress The address that will receive the reissued synthetic tokens
    function EMERGENCY_REISSUE(
        bytes32 _transferId,
        address _recipientAddress
    ) external onlyRole(ADMIN_ROLE) {
        if (_recipientAddress == address(0)) revert InvalidRecipient();

        MetaERC20MessageStruct memory _message = _getTransferRecord(
            _transferId
        );
        if (_message.amount == 0) revert TransferNotFound();

        uint256 localAmount = _convertDecimals(
            _message.amount,
            _message.sourceDecimals,
            tokenDecimals
        );

        _deleteTransfer(_transferId);
        _mint(_recipientAddress, localAmount);

        emit MetaERC20Received(
            _transferId,
            0, // originDomain unknown in admin override
            msg.sender.addressToBytes32(),
            localDomain,
            _recipientAddress.addressToBytes32(),
            _message.amount,
            _message.sourceDecimals,
            MetaERC20MessageType.AdminAction,
            metaERC20Version,
            bytes32(0), // sourceTokenAddress unknown in admin flow
            address(this).addressToBytes32() // destinationTokenAddress (Spoke contract itself)
        );

        emit MetaERC20AdminReissue(_transferId, msg.sender, _recipientAddress);
    }

    /// @notice Updates the security threshold for high-value transfers
    /// @dev Transfers >= this threshold will be routed to the Hub as SecurityRelay
    /// @param newThreshold The new threshold value in local token units
    function setSecurityThreshold(
        uint256 newThreshold
    ) external onlyRole(ADMIN_ROLE) {
        securityThreshold = newThreshold;
        emit SecurityThresholdUpdated(newThreshold);
    }

    /// @notice Updates the hub domain for cross-chain routing
    /// @dev Hub domain must be non-zero and different from local domain. Callable only by ADMIN_ROLE.
    ///      This setting controls which domain is treated as the canonical Hub for routing decisions.
    /// @param newHubDomain The new hub domain ID
    function setHubDomain(
        uint32 newHubDomain
    ) external onlyRole(ADMIN_ROLE) {
        if (newHubDomain == 0 || newHubDomain == localDomain)
            revert InvalidHubDomain();
        hubDomain = newHubDomain;
        emit HubDomainUpdated(newHubDomain);
    }

    /*//////////////////////////////////////////////////////////////
                        HELPER FUNCTIONS (internal)
    //////////////////////////////////////////////////////////////*/

    /// @notice Determines the dispatch parameters for a Spoke-originating transfer
    /// @dev Implements Spoke's routing logic:
    ///      - High-value transfers (>= securityThreshold) to other spokes route through Hub as SecurityRelay
    ///      - Transfers to Hub become UnlockRequest
    ///      - Low-value transfers to other spokes become direct MintRequest
    ///      The destination domain may differ from recipient domain for SecurityRelay routing.
    /// @param _recipientDomain The user's intended final destination domain
    /// @param _message The partially constructed MetaERC20MessageStruct containing amount for threshold check
    /// @return messageType The type of message to send (MintRequest, UnlockRequest, or SecurityRelay)
    /// @return destinationRouterAddress The MetaERC20 contract address on the destination domain
    /// @return destinationDomain The actual domain to send to (hubDomain for SecurityRelay, otherwise _recipientDomain)
    function _resolveDispatchArguments(
        uint32 _recipientDomain,
        MetaERC20MessageStruct memory _message
    )
        internal
        view
        override
        returns (
            MetaERC20MessageType messageType,
            bytes32 destinationRouterAddress,
            uint32 destinationDomain
        )
    {
        if (_recipientDomain == localDomain) revert InvalidDomainLoopback();

        if (
            _message.amount >= securityThreshold &&
            _recipientDomain != hubDomain
        ) {
            // High-value spoke→spoke transfer → route through Hub
            messageType = MetaERC20MessageType.SecurityRelay;
            destinationRouterAddress = metaERC20Addresses[hubDomain];
            destinationDomain = hubDomain;
        } else if (_recipientDomain == hubDomain) {
            // Standard unlock flow
            messageType = MetaERC20MessageType.UnlockRequest;
            destinationRouterAddress = metaERC20Addresses[hubDomain];
            destinationDomain = hubDomain;
        } else {
            // Normal MintRequest to remote Spoke
            messageType = MetaERC20MessageType.MintRequest;
            destinationRouterAddress = metaERC20Addresses[_recipientDomain];
            destinationDomain = _recipientDomain;
        }

        if (destinationRouterAddress == 0) {
            revert MetaERC20NotRegistered(destinationDomain);
        }

        return (messageType, destinationRouterAddress, destinationDomain);
    }
    /// @notice Burns tokens prior to dispatching a cross-chain transfer
    /// @dev Burns synthetic tokens from sender. The ERC20 _burn function handles balance validation.
    /// @param _sender The address initiating the transfer
    /// @param _amount The token amount to be burned in local token units
    function _preDispatchHook(
        address _sender,
        uint256 _amount
    ) internal override {
        _burn(_sender, _amount);
    }
    /*//////////////////////////////////////////////////////////////
                            UPGRADE GAP
//////////////////////////////////////////////////////////////*/

    /// @dev Reserved storage space to allow for layout upgrades in the future
    uint256[50] private __gap;
}
"
    },
    "node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.0;

import "./IERC20Upgradeable.sol";
import "./extensions/IERC20MetadataUpgradeable.sol";
import "../../utils/ContextUpgradeable.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 * For a generic mechanism see {ERC20PresetMinterPauser}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * The default value of {decimals} is 18. To change this, you should override
 * this function so it returns a different value.
 *
 * We have followed general OpenZeppelin Contracts guidelines: functions revert
 * instead returning `false` on failure. This behavior is nonetheless
 * conventional and does not conflict with the expectations of ERC20
 * applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 *
 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
 * functions have been added to mitigate the well-known issues around setting
 * allowances. See {IERC20-approve}.
 */
contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20Upgradeable, IERC20MetadataUpgradeable {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing {
        __ERC20_init_unchained(name_, symbol_);
    }

    function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5.05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the default value returned by this function, unless
     * it's overridden.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    /**
     * @dev Atomically increases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    /**
     * @dev Atomically decreases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `spender` must have allowance for the caller of at least
     * `subtractedValue`.
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev Moves `amount` of tokens from `from` to `to`.
     *
     * This internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     */
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    /** @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, reducing the
     * total supply.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens.
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[45] private __gap;
}
"
    },
    "node_modules/@hyperlane-xyz/core/contracts/libs/TypeCasts.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;

library TypeCasts {
    // alignment preserving cast
    function addressToBytes32(address _addr) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_addr)));
    }

    // alignment preserving cast
    function bytes32ToAddress(bytes32 _buf) internal pure returns (address) {
        require(
            uint256(_buf) <= uint256(type(uint160).max),
            "TypeCasts: bytes32ToAddress overflow"
        );
        return address(uint160(uint256(_buf)));
    }
}
"
    },
    "src/lib/MetalayerMessage.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
import {ReadOperation} from "../interfaces/IMetalayerRecipient.sol";

enum FinalityState {
    INSTANT,
    FINALIZED,
    ESPRESSO
}

/**
 * @title Metalayer Message Library
 * @notice Library for formatted messages for the metalayer. These will be the message body of Hyperlane messages.
 *
 */
library MetalayerMessage {
    using TypeCasts for bytes32;

    /**
     * @notice Safely casts uint256 to uint32, reverting on overflow
     * @param value The value to cast
     * @return The value as uint32
     */
    function safeCastToUint32(uint256 value) internal pure returns (uint32) {
        require(value <= type(uint32).max, "MetalayerMessage: value exceeds uint32 range");
        return uint32(value);
    }

    uint256 private constant VERSION_OFFSET = 0;
    uint256 private constant FINALITY_STATE_FLAG_OFFSET = 1;
    uint256 private constant NONCE_OFFSET = 2;
    uint256 private constant ORIGIN_OFFSET = 6;
    uint256 private constant SENDER_OFFSET = 10;
    uint256 private constant DESTINATION_OFFSET = 42;
    uint256 private constant RECIPIENT_OFFSET = 46;
    uint256 private constant BODY_OFFSET = 78;

    // Constants for read operation parsing within message body
    uint256 private constant READ_COUNT_OFFSET = 0;
    uint256 private constant READS_OFFSET = 4; // uint32 size for read count

    /**
     * @notice Returns formatted message supporting multiple read operations
     * @param _version The version of the origin and destination Mailboxes
     * @param _finalityState What sort of finality we should wait for before the message is valid. Currently only have 0 for instant, 1 for final.
     * @param _nonce A nonce to uniquely identify the message on its origin chain
     * @param _originDomain Domain of origin chain
     * @param _sender Address of sender
     * @param _destinationDomain Domain of destination chain
     * @param _recipient Address of recipient on destination chain
     * @param _reads Array of read operations to perform
     * @param _writeCallData The call data for the final write operation
     * @return Formatted message with reads
     */
    function formatMessageWithReads(
        uint8 _version,
        FinalityState _finalityState,
        uint32 _nonce,
        uint32 _originDomain,
        bytes32 _sender,
        uint32 _destinationDomain,
        bytes32 _recipient,
        ReadOperation[] memory _reads,
        bytes memory _writeCallData
    ) internal pure returns (bytes memory) {
        uint256 _readsLength = _reads.length;
        bytes memory messageBody = abi.encodePacked(safeCastToUint32(_readsLength));

        // this is n^2 -> optimize later by just preallocating the appropriate size
        // can keep slow for test
        for (uint256 i = 0; i < _readsLength; i++) {
            messageBody = abi.encodePacked(
                messageBody, _reads[i].domain, _reads[i].target, safeCastToUint32(_reads[i].callData.length), _reads[i].callData
            );
        }

        messageBody = abi.encodePacked(messageBody, _writeCallData);

        return abi.encodePacked(
            _version, uint8(_finalityState), _nonce, _originDomain, _sender, _destinationDomain, _recipient, messageBody
        );
    }

    /**
     * @notice Returns the message ID.
     * @param _message ABI encoded Metalayer message.
     * @return ID of `_message`
     */
    function id(bytes memory _message) internal pure returns (bytes32) {
        return keccak256(_message);
    }

    /**
     * @notice Returns the message version
     * @param _message ABI encoded Metalayer message
     * @return Version of `_message`
     */
    function version(bytes calldata _message) internal pure returns (uint8) {
        return uint8(bytes1(_message[VERSION_OFFSET:FINALITY_STATE_FLAG_OFFSET]));
    }

    /**
     * @notice Returns the message nonce
     * @param _message ABI encoded Metalayer message
     * @return Nonce of `_message`
     */
    function nonce(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[NONCE_OFFSET:ORIGIN_OFFSET]));
    }

    /**
     * @notice Returns whether the message should use finalized ISM
     * @param _message ABI encoded Metalayer message
     * @return Whether to use finalized ISM
     */
    function finalityState(bytes calldata _message) internal pure returns (FinalityState) {
        return FinalityState(uint8(bytes1(_message[FINALITY_STATE_FLAG_OFFSET:NONCE_OFFSET])));
    }

    /**
     * @notice Returns the message origin domain
     * @param _message ABI encoded Metalayer message
     * @return Origin domain of `_message`
     */
    function origin(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[ORIGIN_OFFSET:SENDER_OFFSET]));
    }

    /**
     * @notice Returns the message sender as address
     * @param _message ABI encoded Metalayer message
     * @return Sender of `_message` as a bytes32-encoded address
     */
    function senderAddress(bytes calldata _message) internal pure returns (bytes32) {
        return bytes32(_message[SENDER_OFFSET:DESTINATION_OFFSET]);
    }

    /**
     * @notice Returns the message destination domain
     * @param _message ABI encoded Metalayer message
     * @return Destination domain of `_message`
     */
    function destination(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[DESTINATION_OFFSET:RECIPIENT_OFFSET]));
    }

    /**
     * @notice Returns the message recipient as address. We only support evm chains for now, so address only.
     * @param _message ABI encoded Metalayer message
     * @return Recipient of `_message` as address
     */
    function recipientAddress(bytes calldata _message) internal pure returns (bytes32) {
        return bytes32(_message[RECIPIENT_OFFSET:BODY_OFFSET]);
    }

    /**
     * @notice Returns the message body
     * @param _message ABI encoded Metalayer message
     * @return Body of `_message`
     */
    function body(bytes calldata _message) internal pure returns (bytes calldata) {
        return bytes(_message[BODY_OFFSET:]);
    }

    /**
     * @notice Returns the number of read operations in the message
     * @param _message ABI encoded Metalayer message
     * @return Number of read operations
     */
    function readCount(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(body(_message)[READ_COUNT_OFFSET:READS_OFFSET]));
    }

    /**
     * @notice Returns the read operation at the specified index
     * @param _message ABI encoded Metalayer message
     * @param _index Index of the read operation to retrieve
     * @return The read operation at the specified index
     */
    function getRead(bytes calldata _message, uint256 _index) internal pure returns (ReadOperation memory) {
        require(_index < readCount(_message), "Index out of bounds");

        bytes calldata messageBody = body(_message);
        uint256 currentOffset = READS_OFFSET;

        for (uint256 i = 0; i < _index; i++) {
            uint256 lesserCallDataLength = uint32(bytes4(messageBody[currentOffset + 24:currentOffset + 28]));
            currentOffset += 28 + lesserCallDataLength; // 4 (domain) + 20 (contract) + 4 (length) + lesserCallDataLength
        }

        // first 4 bytes are the uint32 sourceChainId.
        uint32 sourceChainId = uint32(bytes4(messageBody[currentOffset:currentOffset + 4]));
        // next 20 bytes are the address of the contract to read from. Offset computed as starting after the previous 4 bytes, ending 20 bytes later (==24)
        address sourceContract = address(bytes20(messageBody[currentOffset + 4:currentOffset + 24]));
        // next 4 bytes are the uint32 length of the call data. Offset computed as starting after the previous 24 bytes, ending 4 bytes later (==28)
        uint256 callDataLength = uint32(bytes4(messageBody[currentOffset + 24:currentOffset + 28]));
        // next callDataLength bytes are the call data. Offset computed as starting after the previous 28 bytes, ending callDataLength bytes later (==28 + callDataLength)
        bytes calldata callData = messageBody[currentOffset + 28:currentOffset + 28 + callDataLength];

        return ReadOperation({domain: sourceChainId, target: sourceContract, callData: callData});
    }

    /**
     * @notice Returns all read operations from the message. Use this instead of getRead repeatedly if you want all operations as that is O(n) to find one read, O(n^2) total.
     * @param _message ABI encoded Metalayer message
     * @return Array of all read operations
     */
    function reads(bytes calldata _message) internal pure returns (ReadOperation[] memory) {
        uint256 numReads = readCount(_message);
        ReadOperation[] memory tempReads = new ReadOperation[](numReads);

        bytes calldata messageBody = body(_message);
        uint256 currentOffset = READS_OFFSET;

        for (uint256 i = 0; i < numReads; i++) {
            // first 4 bytes are the uint32 sourceChainId. Offset computed as starting at the current offset, ending 4 bytes later (==4)
            uint32 sourceChainId = uint32(bytes4(messageBody[currentOffset:currentOffset + 4]));
            // next 20 bytes are the address of the contract to read from. Offset computed as starting after the previous 4 bytes, ending 20 bytes later (==24)
            address sourceContract = address(bytes20(messageBody[currentOffset + 4:currentOffset + 24]));
            // next 4 bytes are the uint32 length of the call data. Offset computed as starting after the previous 24 bytes, ending 4 bytes later (==28)
            uint256 callDataLength = uint32(bytes4(messageBody[currentOffset + 24:currentOffset + 28]));
            // next callDataLength bytes are the call data. Offset computed as starting after the previous 28 bytes, ending callDataLength bytes later (==28 + callDataLength)
            bytes calldata callData = messageBody[currentOffset + 28:currentOffset + 28 + callDataLength];

            tempReads[i] = ReadOperation({domain: sourceChainId, target: sourceContract, callData: callData});

            currentOffset += 28 + callDataLength; // 4 (domain) + 20 (contract) + 4 (length) + callDataLength
        }

        return tempReads;
    }

    /**
     * @notice Returns the write call data from the message
     * @param _message ABI encoded Metalayer message
     * @return The write call data
     */
    function writeCallData(bytes calldata _message) internal pure returns (bytes calldata) {
        bytes calldata messageBody = body(_message);
        uint256 currentOffset = READS_OFFSET;

        uint256 numReads = readCount(_message);
        for (uint256 i = 0; i < numReads; i++) {
            // an encoded read operation starts with 4 bytes for the sourceChainId, 20 bytes for the contract address, 4 bytes for the callDataLength, and then the callDataLength bytes for the callData.
            // calldata length can be extracted as the uint32 beginning after the sourceChainId and contract address (20+4=24 bytes in total), and ending 4 bytes later (==28).
            uint256 callDataLength = uint32(bytes4(messageBody[currentOffset + 24:currentOffset + 28]));
            // the next callDataLength bytes are the call data. This read ends after the 28 byte header and the callDataLength bytes.
            currentOffset += 28 + callDataLength;
        }

        return messageBody[currentOffset:];
    }
}
"
    },
    "src/lib/MetaERC20Message.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.26;

import {MetaERC20MessageStruct, MetaERC20MessageType} from "./MetaERC20Types.sol";

/// @title MetaERC20Message
/// @notice Library for encoding and decoding MetaERC20 protocol messages
/// @dev Provides low-level serialization for MetaERC20MessageStruct with fixed 128-byte layout.
///      Uses raw amounts with source decimal metadata instead of pre-scaled values for maximum
///      precision and cross-chain compatibility. Supports tokens with any decimal count.
///
/// Message layout (128 bytes total):
/// - transferId (32B) + timestamp (32B) + version (1B) + messageType (1B)
/// - padding (5B) + sourceDecimals (1B) + recipientDomain (4B)
/// - recipient (20B) + amount (32B)
///
/// Key features:
/// - Fixed-size encoding for predictable gas costs
/// - Comprehensive validation during encoding
/// - Raw amount preservation with decimal metadata
/// - Block explorer friendly field ordering (smaller fields grouped)
/// - Zero-tolerance validation for critical fields
///
library MetaERC20Message {
    /*//////////////////////////////////////////////////////////////
                            CONSTANTS
    //////////////////////////////////////////////////////////////*/

    /// @dev Byte offsets for message field decoding
    uint8 private constant TRANSFERID_OFFSET = 0;
    uint8 private constant TRANSFERID_SIZE = 32;

    uint8 private constant TIMESTAMP_OFFSET = 32;
    uint8 private constant TIMESTAMP_SIZE = 32;

    uint8 private constant VERSION_OFFSET = 64;
    uint8 private constant VERSION_SIZE = 1;

    uint8 private constant MESSAGETYPE_OFFSET = 65;
    uint8 private constant MESSAGETYPE_SIZE = 1;

    // 5 bytes padding here (66-70)

    uint8 private constant SOURCEDECIMALS_OFFSET = 71;
    uint8 private constant SOURCEDECIMALS_SIZE = 1;

    uint8 private constant RECIPIENTDOMAIN_OFFSET = 72;
    uint8 private constant RECIPIENTDOMAIN_SIZE = 4;

    uint8 private constant RECIPIENT_OFFSET = 76;
    uint8 private constant RECIPIENT_SIZE = 20;

    uint8 private constant AMOUNT_OFFSET = 96;
    uint8 private constant AMOUNT_SIZE = 32;

    /// @dev Fixed size of a MetaERC20MessageStruct when encoded (128 bytes)
    uint8 private constant MESSAGE_SIZE = 128;

    /*//////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/
    /// @notice Thrown when attempting to decode a message shorter than MESSAGE_SIZE
    error IncompleteMessage();

    /// @notice Thrown when encoding a message with an unknown or out-of-bounds messageType
    error InvalidMessageType(uint8 messageType);

    /// @notice Thrown when encoding a message with version == 0 (unsupported)
    error UnsupportedVersion(uint8 version);

    /// @notice Thrown when encoding a message with recipientDomain == 0
    error ZeroRecipientDomain();

    /// @notice Thrown when encoding a message with timestamp == 0
    error ZeroTimestamp();

    /// @notice Thrown when attempting to encode a message with zero transferId
    error ZeroTransferId();

    /// @notice Thrown when encoding a message with a zero recipient address
    error ZeroRecipient();

    /*//////////////////////////////////////////////////////////////
                            DECODERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Decodes a MetaERC20 message from raw calldata
    /// @param _data ABI-encoded message bytes (expected length = MESSAGE_SIZE)
    /// @return msgStruct Decoded MetaERC20MessageStruct
    function decodeMessage(
        bytes calldata _data
    ) internal pure returns (MetaERC20MessageStruct memory msgStruct) {
        if (_data.length < MESSAGE_SIZE) revert IncompleteMessage();

        msgStruct.transferId = bytes32(
            _data[TRANSFERID_OFFSET:TRANSFERID_OFFSET + TRANSFERID_SIZE]
        );
        msgStruct.timestamp = uint256(
            bytes32(_data[TIMESTAMP_OFFSET:TIMESTAMP_OFFSET + TIMESTAMP_SIZE])
        );
        msgStruct.metaERC20Version = uint8(_data[VERSION_OFFSET]);
        msgStruct.messageType = MetaERC20MessageType(
            uint8(_data[MESSAGETYPE_OFFSET])
        );
        msgStruct.sourceDecimals = uint8(_data[SOURCEDECIMALS_OFFSET]);
        msgStruct.recipientDomain = uint32(
            bytes4(
                _data[RECIPIENTDOMAIN_OFFSET:RECIPIENTDOMAIN_OFFSET +
                    RECIPIENTDOMAIN_SIZE]
            )
        );
        msgStruct.recipient = address(
            bytes20(_data[RECIPIENT_OFFSET:RECIPIENT_OFFSET + RECIPIENT_SIZE])
        );
        msgStruct.amount = uint256(
            bytes32(_data[AMOUNT_OFFSET:AMOUNT_OFFSET + AMOUNT_SIZE])
        );

        return msgStruct;
    }

    /*//////////////////////////////////////////////////////////////
                            ENCODERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Encodes a MetaERC20MessageStruct into bytes
    /// @param _message The fully populated message struct
    /// @return Encoded bytes representation of the message (length = MESSAGE_SIZE)
    function encodeMessage(
        MetaERC20MessageStruct memory _message
    ) internal pure returns (bytes memory) {
        if (_message.transferId == bytes32(0)) revert ZeroTransferId();
        if (_message.timestamp == 0) revert ZeroTimestamp();
        if (_message.metaERC20Version == 0) revert UnsupportedVersion(0);
        if (
            uint8(_message.messageType) >=
            uint8(MetaERC20MessageType.__MessageTypeCount)
        ) revert InvalidMessageType(uint8(_message.messageType));
        if (_message.recipientDomain == 0) revert ZeroRecipientDomain();
        if (_message.recipient == address(0)) revert ZeroRecipient();

        return
            abi.encodePacked(
                _message.transferId, // 32 bytes (0-31)
                _message.timestamp, // 32 bytes (32-63)
                _message.metaERC20Version, // 1 byte  (64)
                uint8(_message.messageType), // 1 byte  (65)
                bytes5(0), // 5 bytes padding (66-70)
                _message.sourceDecimals, // 1 byte  (71)
                _message.recipientDomain, // 4 bytes (72-75)
                _message.recipient, // 20 bytes (76-95)
                _message.amount // 32 bytes (96-127)
            ); // Total: 128 bytes
    }
}
"
    },
    "src/lib/MetaERC20Types.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.26;

/// @title MetaERC20MessageType
/// @notice Enumerates supported MetaERC20 message intents for cross-chain dispatch
enum MetaERC20MessageType {
    /// @notice Mint synthetic tokens on the destination chain
    MintRequest,
    /// @notice Unlock canonical tokens on the destination chain
    UnlockRequest,
    /// @notice Relays a Spoke-to-Spoke transfer via the Hub for security inspection
    SecurityRelay,
    /// @notice Admin-triggered reissuance or override action
    AdminAction,
    /// @dev Sentinel value for bounds checking and enum size
    __MessageTypeCount
}

/// @title MetaERC20MessageStruct
/// @notice Fully packed message used for MetaERC20 cross-chain communication
/// @dev Serialized as a 128-byte abi.encodePacked payload with no dynamic fields.
///      Layout: transferId(32) + timestamp(32) + version(1) + messageType(1) +
///      padding(5) + sourceDecimals(1) + recipientDomain(4) + recipient(20) + amount(32)
struct MetaERC20MessageStruct {
    /// @notice Unique ID for this transfer, generated deterministically at source
    bytes32 transferId;
    /// @notice Local block.timestamp when the message was created at source
    uint256 timestamp;
    /// @notice Version of the MetaERC20 message protocol used
    uint8 metaERC20Version;
    /// @notice Type of message intent (MintRequest, UnlockRequest, SecurityRelay, AdminAction)
    MetaERC20MessageType messageType;
    // 5 bytes padding (bytes 66-70)

    /// @notice Number of decimal places used by the source token (e.g., 6 for USDC, 18 for WETH)
    uint8 sourceDecimals;
    /// @notice Metalayer domain ID of the intended final destination chain
    uint32 recipientDomain;
    /// @notice Address that will receive the tokens on the destination chain
    address recipient;
    /// @notice Amount of tokens in source token's native units (no decimal scaling applied)
    uint256 amount;
}
"
    },
    "src/interfaces/IMetalayerRecipient.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;

/**
 * @title IMetalayerRecipient
 * @notice Interface for contracts that can receive messages through the Metalayer protocol
 * @dev Implement this interface to receive cross-chain messages and read results from Metalayer
 */
interface IMetalayerRecipient {
    /**
     * @notice Handles an incoming message from another chain via Metalayer
     * @dev This function is called by the MetalayerRouter when a message is delivered
     * @param _origin The domain ID of the chain where the message originated
     * @param _sender The address of the contract that sent the message on the origin chain
     * @param _message The payload of the message to be handled
     * @param _reads Array of read operations that were requested in the original message
     * @param _readResults Array of results from the read operations, provided by the relayer
     * @custom:security The caller must be the MetalayerRouter contract
     */
    function handle(
        uint32 _origin,
        bytes32 _sender,
        bytes calldata _message,
        ReadOperation[] calldata _reads,
        bytes[] calldata _readResults
    ) external payable;
}

/**
 * @notice Represents a cross-chain read operation
 * @dev Used to specify what data should be read from other chains.
 * The read operations are only compatible with EVM chains, so the
 * target is packed as an address to save bytes.
 */
struct ReadOperation {
    /// @notice The domain ID of the chain to read from
    uint32 domain;
    /// @notice The address of the contract to read from
    address target;
    /// @notice The calldata to execute on the target contract
    bytes callData;
}
"
    },
    "src/token/MetaERC20Base.sol": {
      "content": "// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.26;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {AccessControlDefaultAdminRulesUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlDefaultAdminRulesUpgradeable.sol";

// for sweep functions
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";

import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";

import {IMetalayerRouter} from "../interfaces/IMetalayerRouter.sol";
import {MetalayerMessage, FinalityState} from "../lib/MetalayerMessage.sol";
import {MetaERC20Message} from "../lib/MetaERC20Message.sol";
import {MetaERC20MessageStruct, MetaERC20MessageType} from "../lib/MetaERC20Types.sol";
import {IMetalayerRecipient, ReadOperation} from "../interfaces/IMetalayerRecipient.sol";

/// @title MetaERC20Base
/// @notice Abstract base contract for MetaERC20Hub and MetaERC20Spoke
/// @dev Implements shared storage, decimal conversion logic, message dispatch, and upgradeable behavior.
///      Inheriting contracts must override validation and message handling hooks to enforce custom routing logic.
///      Uses raw amounts with source decimal metadata instead of pre-scaling for maximum precision and flexibility.
///
/// Key features:
/// - Configurable gas limits with admin controls
/// - Efficient O(1) domain registration using dual-mapping pattern
/// - CEI pattern throughout for reentrancy protection
/// - Transfer record management with TTL-based pruning
/// - Replay protection using transferId tracking
///
/// Skipped features:
/// - supportsInterface(...) - Not needed for current use case
/// - reclaimETH or sweep() - No ETH handling planned
/// - quoteDispatch(...) - Gas estimation handled externally
///
/// @custom:oz-upgrades-unsafe-allow constructor
abstract contract MetaERC20Base is
    Initializable,
    AccessControlDefaultAdminRulesUpgradeable,
    IMetalayerRecipient
{
    using SafeERC20Upgradeable for IERC20Upgradeable;
    using TypeCasts for bytes32;
    using TypeCasts for address;

    /*//////////////////////////////////////////////////////////////
                            CONSTANTS
    //////////////////////////////////////////////////////////////*/

    bytes32 public constant ADMIN_ROLE = keccak256("METAERC20ADMIN_ROLE");

    /// @notice Maximum number of transfer IDs allowed in a batch prune
    /// @dev Used in batchPruneTransfers to limit gas usage
    uint256 public constant MAX_PRUNE_BATCH = 100;

    /// @notice Maximum number of transfer IDs allowed in a batch query
    /// @dev Used in getTransferRecords to prevent excessive RPC response sizes
    uint256 public constant MAX_QUERY_BATCH = 500;

    /// @notice Maximum number of domains that can be registered in a single batch call
    /// @dev Used in setDomainAddressBatch to prevent out-of-gas errors
    uint256 public constant MAX_DOMAIN_BATCH = 50;

    /// @notice Default maximum gas limit (can be adjusted by admin)
    uint256 public constant DEFAULT_MAX_GAS_LIMIT = 10_000_000; // 10M = 1/3 of block

    /*//////////////////////////////////////////////////////////////
                            VARIABLES
    //////////////////////////////////////////////////////////////*/

    /*//////////////////////////////////////////////////////////////
                        STORAGE LAYOUT (SLOT MAP)
    ---------------------------------------------------------------
    | Slot  | Field                    | Size   | Notes           |
    |-------|--------------------------|--------|-----------------|
    | 0     | _initialized             | 1 B    |                 |
    |       | _initializing            | 1 B    |                 |
    |       | unused                   | 30 B   |                 |
    | 1     | _supportedInterfaces     | 32 B   |                 |
    | 2     | _roles                   | 32 B   |                 |
    | 3     | _pendingDefaultAdmin     | 26 B   |                 |
    |       | unused                   | 6 B    |                 |
    | 4     | _currentDelay            | 6 B    |                 |
    |       | _currentAdmin            | 20 B   |                 |
    |       | unused                   | 8 B    |                 |
    | 5     | _pendingDelay            | 12 B   |                 |
    |-------|--------------------------|--------|-----------------|
    | 6     | metalayerRouter          | 20 B   |                 |
    |       | localDomain              | 4 B    |                 |
    | 7     | ttlWindow                | 32 B   |                 |
    | 8     | transferNonce            | 32 B   |                 |
    | 9     | maxGasLimit              | 32 B   |                 |
    | 10    | __reservedSlot5          | 30 B   | Padding         |
    |       | tokenDecimals            | 1 B    |                 |
    |       | metaERC20Version         | 1 B    |                 |
    | 11    | _transferRecords         | —      | Mapping anchor  |
    | 12    | executedTransfers        | —      | Mapping anchor  |
    | 13    | metaERC20Addresses       | —      | Mapping anchor  |
    | 14    | registeredDomainByIndex  | —      | Mapping anchor  |
    | 15    | registeredDomainIndex    | —      | Mapping anchor  |
    | 16    | registeredDomainCount    | 32 B   |                 |
    | 17–66 | __gap                    | 50x32B | Reserved (Base) |
    ---------------------------------------------------------------
    Total declared slots: 67 (17 + 50 gap)
    //////////////////////////////////////////////////////////////*/

    /// @notice Domain ID of this contract's local chain (Hyperlane domain, not EVM chainid)
    uint32 public localDomain;

    /// @notice Address of the MetalayerRouter used to dispatch and receive cross-chain messages
    IMetalayerRouter public metalayerRouter;

    /// @notice Number of seconds after which a transfer is eligible for pruning
    uint256 public ttlWindow;

    /// @notice Monotonically increasing nonce used for transfer ID generation
    uint256 public transferNonce;

    /// @notice Maximum gas limit allowed for cross-chain dispatch
    /// @dev Prevents accidental or malicious consumption of entire block gas
    uint256 public maxGasLimit;

    /// @notice Pad top of slot 5 for alignment; tokenDecimals and metaERC20Version share the remaining bytes
    uint240 private __reservedSlot5;

    /// @notice Number of decimals the token uses for unit conversion
    uint8 public tokenDecimals;

    /// @notice Current MetaERC20 message version used for outbound messages
    uint8 public metaERC20Version;

    // Track tokens locked or burnt for handling failed mints
    mapping(bytes32 => MetaERC20MessageStruct) internal _transferRecords;

    /// @notice Tracks whether a given MetaERC20 transferId has been successfully executed
    /// @dev Used to enforce idempotent handling of incoming messages and prevent double execution
    mapping(bytes32 => bool) public executedTransfers;

    /// @notice Maps a Metalayer domain ID to the expected MetaERC20 contract address on that domain
    /// @dev Used for dynamic routing and message validation
    mapping(uint32 domain => bytes32 metaERC20Address)
        public metaERC20Addresses;

    /// @notice Mapping from index to domain ID for efficient enumeration of registered domains
    /// @dev Used with registeredDomainIndex to provide O(1) add operations while preserving enumeration capability
    mapping(uint256 => uint32) public registeredDomainByIndex; // index → domain

    /// @notice Mapping from domain ID to index for efficient lookups and validation
    /// @dev Used to check if a domain is already registered and to locate its position for potential future operations
    mapping(uint32 => uint256) public registeredDomainIndex; // domain → index

    /// @notice Total number of domains currently registered in the system
    /// @dev Incremented when new domains are added. Used as the next available index and for bounds checking.
    uint256 public registeredDomainCount;

    /*//////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    /// @notice Thrown when a duplicate MetaERC20 message is received and already processed
    error AlreadyExecuted();

    /// @notice Thrown when domain and address arrays differ in length
    error ArrayLengthMismatch();

    /// @notice Thrown when setDomainAddressBatch exceeds MAX_DOMAIN_BATCH
    error ExceedsDomainBatchLimit();

    /// @notice Thrown when batchPruneTransfers is called with more than MAX_PRUNE_BATCH IDs
    error ExceedsPruneBatchLimit();

    /// @notice Thrown when getTransferRecords is called with too many IDs
    error ExceedsQueryBatchLimit();

    /// @notice Thrown when a provided domain ID is zero or otherwise invalid
    error InvalidDomain();

    /// @notice Thrown when attempting to dispatch to the local domain (loopback)
    error InvalidDomainLoopback();

    /// @notice Thrown when an invalid or unsupported finality state is provided
    /// @param state The invalid finality state value
    error InvalidFinalityState(uint8 state);

    /// @notice T

Tags:
ERC20, ERC165, Multisig, Upgradeable, Multi-Signature, Factory|addr:0x1e3b8bde411f08446d928baf0141faeffc138181|verified:true|block:23642451|tx:0x6b73de2c3d7fe23862222a9d823f19a2c07a25f443ee276200f495fdd1b580e9|first_check:1761314506

Submitted on: 2025-10-24 16:01:49

Comments

Log in to comment.

No comments yet.