MultiTokenNtt

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/MultiTokenNtt/MultiTokenNtt.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "../GmpManager/GmpManager.sol";
import "../GmpManager/GmpIntegration.sol";
import "../libraries/MultiTokenRateLimiter.sol";
import "../libraries/TokenId.sol";
import "../libraries/TokenMeta.sol";
import "../libraries/TokenInfo.sol";
import "../libraries/NativeTokenTransferCodec.sol";
import "./Peers.sol";
import "../interfaces/IERC20Burnable2.sol";
import "../interfaces/INttTokenReceiver.sol";

import "../interfaces/IWETH.sol";
import "../libraries/TokenDeployment.sol";

import {Token} from "./Token.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "wormhole-solidity-sdk/Utils.sol";
import "wormhole-solidity-sdk/libraries/BytesParsing.sol";

import "../libraries/Implementation.sol";
import "../libraries/PausableOwnable.sol";
import "../libraries/external/ReentrancyGuardUpgradeable.sol";

import "../interfaces/INttToken.sol";

contract MultiTokenNtt is
    MultiTokenRateLimiter,
    GmpIntegration,
    Peers,
    PausableOwnable,
    ReentrancyGuardUpgradeable,
    Implementation
{
    using SafeERC20 for IERC20;
    using TrimmedAmountLib for uint256;
    using TrimmedAmountLib for TrimmedAmount;

    address immutable deployer;
    address public immutable tokenImplementation; // TODO: maybe beacon proxy? (and make it mutable)
    uint16 immutable chainId;
    IWETH public immutable WETH;

    enum Mode {
        LOCKING,
        BURNING
    }

    error ZeroAmount();
    error StaticcallFailed();
    error InvalidRefundAddress();
    error InvalidRecipient();
    error BurnAmountDifferentThanBalanceDiff(uint256 balanceBefore, uint256 balanceAfter);
    error TransferAmountHasDust(uint256 amount, uint256 dust);
    error RefundFailed(uint256 refundAmount);
    error InvalidSender();
    error UnexpectedDeployer(address expectedOwner, address owner);
    error UnexpectedMsgValue();
    error InvalidTargetChain(uint16 targetChain, uint16 chainId);
    error CancellerNotSender(address sender, address canceller);
    error FailedToDeployToken();
    error NttTokenReceiverCallFailed(address recipient);
    error PayloadTooLong(uint256 length);
    error TransferFailed();
    error QueuedTransferWithPayload();
    error InvalidTokenId();
    error CannotOverrideNativeToken();
    error LocalTokenAlreadyRepresentsDifferentAsset();
    error TokenNotRegistered(uint16 chainId, bytes32 tokenAddress);

    event TransferSent(
        uint64 sequence,
        uint16 indexed tokenChain,
        bytes32 indexed token,
        bytes32 recipient,
        bytes32 refundAddress,
        uint256 amount,
        uint16 indexed toChain,
        address sender
    );

    event OutboundTransferCancelled(uint256 sequence, address recipient, uint256 amount);
    event TransferRedeemed(bytes32 indexed digest);

    string public constant NTT_MANAGER_VERSION = "1.1.0"; // TODO: change this

    // =============== Setup =================================================================

    constructor(
        IGmpManager _gmpManager,
        uint64 _rateLimitDuration,
        bool _skipRateLimiting,
        address _tokenImplementation,
        address _weth
    ) MultiTokenRateLimiter(_rateLimitDuration, _skipRateLimiting) GmpIntegration(_gmpManager) {
        deployer = msg.sender;
        chainId = GmpManager(address(_gmpManager)).chainId();
        tokenImplementation = _tokenImplementation;
        WETH = IWETH(_weth);
    }

    function _migrate() internal virtual override {
        // no-op
    }

    function __NttManager_init() internal onlyInitializing {
        // check if the owner is the deployer of this contract
        if (msg.sender != deployer) {
            revert UnexpectedDeployer(deployer, msg.sender);
        }
        if (msg.value != 0) {
            revert UnexpectedMsgValue();
        }
        __PausedOwnable_init(msg.sender, msg.sender);
        __ReentrancyGuard_init();
    }

    function _initialize() internal virtual override {
        super._initialize();
        __NttManager_init();
    }

    // =============== Admin ==============================================================

    function setOutboundLimit(TokenId calldata token, uint256 limit) external onlyOwner {
        uint8 toDecimals = _tokenDecimals(token);
        _setOutboundLimit(token, limit.trim(toDecimals, toDecimals));
    }

    function setInboundLimit(
        TokenId calldata token,
        uint256 limit,
        uint16 chainId_
    ) external onlyOwner {
        uint8 toDecimals = _tokenDecimals(token);
        _setInboundLimit(token, limit.trim(toDecimals, toDecimals), chainId_);
    }

    function setPeer(uint16 _chainId, bytes32 peerAddress) external onlyOwner {
        _setPeer(_chainId, peerAddress);
    }

    function upgrade(
        address newImplementation
    ) external onlyOwner {
        _upgrade(newImplementation);
    }

    /// @notice Override a representation token for a foreign token.
    /// WARNING: if the representation token already exists, this will overwrite it.
    function overrideLocalAsset(TokenId calldata token, address localToken) external onlyOwner {
        if (token.chainId == 0 || token.tokenAddress == bytes32(0)) {
            revert InvalidTokenId();
        }
        if (token.chainId == chainId) {
            revert CannotOverrideNativeToken();
        }
        TokenId memory existing = _getForeignTokenStorage()[localToken];
        if (existing.tokenAddress != bytes32(0)) {
            if (existing.chainId != token.chainId || existing.tokenAddress != token.tokenAddress) {
                revert LocalTokenAlreadyRepresentsDifferentAsset();
            }
        }
        // TODO: should we check if the token exists, and if so, that the metadata didn't change?

        // clean up existing entry if there is one
        address oldToken = _getLocalTokenStorage()[token.chainId][token.tokenAddress].token;
        if (oldToken != address(0)) {
            delete _getForeignTokenStorage()[oldToken];
        }

        _getLocalTokenStorage()[token.chainId][token.tokenAddress] =
            LocalTokenInfo({token: localToken});
        _getForeignTokenStorage()[localToken] = token;
    }

    /// ============== Invariants =============================================

    /// @dev When we add new immutables, this function should be updated
    function _checkImmutables() internal view override {
        // NOTE: we don't check `deployer`, because we don't require the new
        // implementation to be deployed by the same address as the old one, and
        // it's only used for initialization, which can only be called once
        // anyway.
        // NOTE: we also don't check `chainId` because it's derived from
        // gmpManager in a deterministic way (i.e. can't be overridden in a new deployment)
        super._checkImmutables();
        assert(this.gmpManager() == gmpManager);
        assert(this.rateLimitDuration() == rateLimitDuration);
        assert(this.tokenImplementation() == tokenImplementation);
        assert(this.WETH() == WETH);
    }

    // ==================== External Interface ===============================================

    enum WrapETH {
        Wrap,
        NoWrap
    }

    /// @notice Transfer tokens to another chain with comprehensive parameter support
    struct TransferArgs {
        address token;
        uint256 amount;
        uint16 recipientChain;
        bytes32 recipient;
        bytes32 refundAddress;
        bool shouldQueue;
        bytes transceiverInstructions;
        bytes additionalPayload;
    }

    function transfer(
        TransferArgs memory args
    ) external payable nonReentrant whenNotPaused returns (uint64) {
        return _executeTransferWithArgs(args, WrapETH.NoWrap);
    }

    /// @notice Transfer gas token (ETH) to another chain with comprehensive parameter support
    struct GasTokenTransferArgs {
        uint256 amount;
        uint16 recipientChain;
        bytes32 recipient;
        bytes32 refundAddress;
        bool shouldQueue;
        bytes transceiverInstructions;
        bytes additionalPayload;
    }

    function wrapAndTransferGasToken(
        GasTokenTransferArgs memory args
    ) external payable nonReentrant whenNotPaused returns (uint64) {
        TransferArgs memory transferArgs = TransferArgs({
            token: address(WETH),
            amount: args.amount,
            recipientChain: args.recipientChain,
            recipient: args.recipient,
            refundAddress: args.refundAddress,
            shouldQueue: args.shouldQueue,
            transceiverInstructions: args.transceiverInstructions,
            additionalPayload: args.additionalPayload
        });

        return _executeTransferWithArgs(transferArgs, WrapETH.Wrap);
    }

    // ========== Internal Helper Functions ==========

    function _executeTransferWithArgs(
        TransferArgs memory args,
        WrapETH wrapMode
    ) internal returns (uint64) {
        // Set default values for optional parameters
        bytes32 finalRefundAddress =
            args.refundAddress == bytes32(0) ? args.recipient : args.refundAddress;
        bytes memory finalInstructions =
            args.transceiverInstructions.length == 0 ? new bytes(1) : args.transceiverInstructions;

        TransferArgs memory finalArgs = args;
        finalArgs.refundAddress = finalRefundAddress;
        finalArgs.transceiverInstructions = finalInstructions;

        return _transferEntryPoint(
            TransferParams({msgValue: msg.value, wrapWETH: wrapMode, args: finalArgs})
        );
    }

    function _receiveMessage(
        bytes32 digest,
        uint16 sourceChainId,
        bytes32 sender,
        bytes calldata data
    ) internal override {
        _verifyPeer(sourceChainId, sender);
        // parse the data into a NativeTokenTransfer
        NativeTokenTransferCodec.NativeTokenTransfer memory message =
            NativeTokenTransferCodec.parseNativeTokenTransfer(data);
        _executeMsg(digest, sourceChainId, message);
    }

    /// @dev Get the creation code for the ERC1967Proxy contract
    /// used to deploy new tokens. This might be useful for client code that
    /// wants to compute the CREATE2 address.
    /// Exposing this as a view function makes the client side code easier to
    /// manage, as the generated bytecode changes depending on compiler flags.
    function tokenProxyCreationCode() external pure returns (bytes memory) {
        return TokenDeployment.getTokenProxyCreationCode();
    }

    function _executeMsg(
        bytes32 digest,
        uint16 sourceChainId,
        NativeTokenTransferCodec.NativeTokenTransfer memory nativeTokenTransfer
    ) internal whenNotPaused nonReentrant {
        bytes20 transferDigest = bytes20(
            keccak256(NativeTokenTransferCodec.encodeNativeTokenTransfer(nativeTokenTransfer))
        );

        TokenInfo memory info = nativeTokenTransfer.token;

        address token = _getOrCreateToken(info);

        uint8 toDecimals = _tokenDecimals(info.token);
        TrimmedAmount nativeTransferAmount =
            (nativeTokenTransfer.amount.untrim(toDecimals)).trim(toDecimals, toDecimals);

        address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to);

        if (!_getInboundLimitParamsStorage(info.token)[sourceChainId].limit.isNull()) {
            // Check inbound rate limits
            bool isRateLimited =
                _isInboundAmountRateLimited(info.token, nativeTransferAmount, sourceChainId);
            if (isRateLimited) {
                // queue up the transfer
                _enqueueInboundTransfer(digest, sourceChainId, transferDigest);

                // end execution early
                return;
            }

            // consume the amount for the inbound rate limit
            _consumeInboundAmount(info.token, nativeTransferAmount, sourceChainId);
        }

        // When receiving a transfer, we refill the outbound rate limit
        // by the same amount (we call this "backflow")
        if (!_getOutboundLimitParamsStorage(info.token).limit.isNull()) {
            // if the outbound limit is not set, we don't backfill
            // this is to avoid backfilling for tokens that don't have an outbound limit set
            _backfillOutboundAmount(info.token, nativeTransferAmount);
        }

        _mintOrUnlockToRecipient(
            digest,
            token,
            transferRecipient,
            nativeTransferAmount,
            false,
            nativeTokenTransfer.additionalPayload,
            sourceChainId,
            nativeTokenTransfer.sender
        );
    }

    function completeInboundQueuedTransfer(
        bytes32 digest,
        NativeTokenTransferCodec.NativeTokenTransfer memory nativeTokenTransfer
    ) external nonReentrant whenNotPaused {
        // compute the digest from the provided transfer
        bytes20 transferDigest = bytes20(
            keccak256(NativeTokenTransferCodec.encodeNativeTokenTransfer(nativeTokenTransfer))
        );

        // find the message in the queue
        InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest);
        if (queuedTransfer.txTimestamp == 0) {
            revert InboundQueuedTransferNotFound(digest);
        }

        if (queuedTransfer.transferDigest != transferDigest) {
            revert InboundQueuedTransferDigestMismatch(
                transferDigest, queuedTransfer.transferDigest
            );
        }

        // check that > RATE_LIMIT_DURATION has elapsed
        if (block.timestamp - queuedTransfer.txTimestamp < rateLimitDuration) {
            revert InboundQueuedTransferStillQueued(digest, queuedTransfer.txTimestamp);
        }

        // remove transfer from the queue
        delete _getInboundQueueStorage()[digest];

        // get token and recipient from the provided transfer struct
        address token = _getOrCreateToken(nativeTokenTransfer.token);
        address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to);

        uint8 toDecimals = _tokenDecimals(nativeTokenTransfer.token.token);
        TrimmedAmount nativeTransferAmount =
            (nativeTokenTransfer.amount.untrim(toDecimals)).trim(toDecimals, toDecimals);

        // run it through the mint/unlock logic
        _mintOrUnlockToRecipient(
            digest,
            token,
            transferRecipient,
            nativeTransferAmount,
            false,
            nativeTokenTransfer.additionalPayload,
            queuedTransfer.sourceChainId,
            nativeTokenTransfer.sender
        );
    }

    function completeOutboundQueuedTransfer(
        uint64 messageSequence
    ) external payable nonReentrant whenNotPaused returns (uint64) {
        // find the message in the queue
        OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
        if (queuedTransfer.txTimestamp == 0) {
            revert OutboundQueuedTransferNotFound(messageSequence);
        }

        // check that > RATE_LIMIT_DURATION has elapsed
        if (block.timestamp - queuedTransfer.txTimestamp < rateLimitDuration) {
            revert OutboundQueuedTransferStillQueued(messageSequence, queuedTransfer.txTimestamp);
        }

        // remove transfer from the queue
        delete _getOutboundQueueStorage()[messageSequence];

        // run it through the transfer logic and skip the rate limit
        return _transfer(
            msg.value,
            messageSequence,
            queuedTransfer.token,
            queuedTransfer.amount,
            queuedTransfer.recipientChain,
            queuedTransfer.recipient,
            queuedTransfer.refundAddress,
            queuedTransfer.sender,
            queuedTransfer.transceiverInstructions,
            "" // Queued transfers don't support additional payload
        );
    }

    function cancelOutboundQueuedTransfer(
        uint64 messageSequence
    ) external nonReentrant whenNotPaused {
        // find the message in the queue
        OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
        if (queuedTransfer.txTimestamp == 0) {
            revert OutboundQueuedTransferNotFound(messageSequence);
        }

        // check msg.sender initiated the transfer
        if (queuedTransfer.sender != msg.sender) {
            revert CancellerNotSender(msg.sender, queuedTransfer.sender);
        }

        // remove transfer from the queue
        delete _getOutboundQueueStorage()[messageSequence];

        // return the queued funds to the sender
        _mintOrUnlockToRecipient(
            bytes32(uint256(messageSequence)),
            queuedTransfer.token,
            msg.sender,
            queuedTransfer.amount,
            true,
            "", // No additional payload for cancelled transfers
            0, // No source chain info for cancelled transfers
            bytes32(0) // No source address for cancelled transfers
        );
    }

    /// @dev This function is called when the contract receives ETH
    ///     It is required for unwrapping WETH
    receive() external payable {}

    // ==================== Internal Business Logic =========================================

    // TODO: move to utils?
    function _refundToSender(
        uint256 refundAmount
    ) internal {
        // refund the price quote back to sender
        (bool refundSuccessful,) = payable(msg.sender).call{value: refundAmount}("");

        // check success
        if (!refundSuccessful) {
            revert RefundFailed(refundAmount);
        }
    }

    // TODO: maybe store information about every token, not just foreign ones?
    // specifically, it might make sense to store whether the token is rate
    // limited (since we're already hitting this storage slot, it would be more
    // efficient than hitting another one... although that's not as nice from an
    // encapsulation perspective, so who knows.)
    bytes32 private constant FOREIGN_TOKEN_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.foreignTokenInfo")) - 1);

    function _getForeignTokenStorage()
        internal
        pure
        returns (mapping(address => TokenId) storage $)
    {
        bytes32 slot = FOREIGN_TOKEN_SLOT;
        assembly {
            $.slot := slot
        }
    }

    function getTokenId(
        address token
    ) public view returns (TokenId memory, Mode) {
        TokenId memory result = _getForeignTokenStorage()[token];
        if (result.chainId != 0) {
            // NOTE: chainId == 0 means the entry is not populated, which means
            // that the token is a local token. This is guaranteed because the
            // entry populated at the time of token creation
            return (result, Mode.BURNING);
        }
        result.chainId = chainId;
        result.tokenAddress = toWormholeFormat(token);
        return (result, Mode.LOCKING);
    }

    bytes32 private constant LOCAL_TOKEN_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.localTokenInfo")) - 1);

    struct LocalTokenInfo {
        address token;
    }

    function _getLocalTokenStorage()
        internal
        pure
        returns (mapping(uint16 => mapping(bytes32 => LocalTokenInfo)) storage $)
    {
        bytes32 slot = LOCAL_TOKEN_SLOT;
        assembly {
            $.slot := slot
        }
    }

    struct TransferParams {
        uint256 msgValue;
        WrapETH wrapWETH;
        TransferArgs args;
    }

    // NOTE: protect every caller with nonReentrant, as this function makes external calls
    function _transferEntryPoint(
        TransferParams memory params
    ) internal returns (uint64) {
        (TokenId memory tokenId, Mode mode) = getTokenId(params.args.token);

        if (params.args.amount == 0) {
            revert ZeroAmount();
        }

        if (params.args.recipient == bytes32(0)) {
            revert InvalidRecipient();
        }

        if (params.args.refundAddress == bytes32(0)) {
            revert InvalidRefundAddress();
        }

        {
            // Lock/burn tokens before checking rate limits
            // use transferFrom to pull tokens from the user and lock them
            // query own token balance before transfer
            uint256 balanceBefore = _getTokenBalanceOf(params.args.token, address(this));

            if (params.wrapWETH == WrapETH.Wrap && params.args.token == address(WETH)) {
                // transfer WETH
                IWETH(WETH).deposit{value: params.args.amount}();
                params.msgValue -= params.args.amount;
            } else {
                IERC20(params.args.token).safeTransferFrom(
                    msg.sender, address(this), params.args.amount
                );
            }

            // query own token balance after transfer
            uint256 balanceAfter = _getTokenBalanceOf(params.args.token, address(this));

            // correct amount for potential transfer fees
            params.args.amount = balanceAfter - balanceBefore;
            if (mode == Mode.BURNING) {
                {
                    // NOTE: We don't account for burn fees in this code path.
                    // We verify that the user's change in balance is equal to the amount that's burned.
                    // Accounting for burn fees can be non-trivial, since there
                    // is no standard way to account for the fee if the fee amount
                    // is taken out of the burn amount.
                    // For example, if there's a fee of 1 which is taken out of the
                    // amount, then burning 20 tokens would result in a transfer of only 19 tokens.
                    // However, the difference in the user's balance would only show 20.
                    // Since there is no standard way to query for burn fee amounts with burnable tokens,
                    // and NTT would be used on a per-token basis, implementing this functionality
                    // is left to integrating projects who may need to account for burn fees on their tokens.
                    try ERC20Burnable(params.args.token).burn(params.args.amount) {}
                    catch {
                        IERC20Burnable2(params.args.token).burn(address(this), params.args.amount);
                    }

                    // tokens held by the contract after the operation should be the same as before
                    uint256 balanceAfterBurn = _getTokenBalanceOf(params.args.token, address(this));
                    if (balanceBefore != balanceAfterBurn) {
                        revert BurnAmountDifferentThanBalanceDiff(balanceBefore, balanceAfterBurn);
                    }
                }
            }
        }

        // trim amount after burning to ensure transfer amount matches (amount - fee)
        TrimmedAmount trimmedAmount;
        // get the sequence for this transfer
        {
            TrimmedAmount internalAmount;
            {
                uint8 decimals = _tokenDecimals(tokenId);
                trimmedAmount = _trimTransferAmount(decimals, params.args.amount);
                internalAmount = trimmedAmount.shift(decimals);
            }

            if (!_getOutboundLimitParamsStorage(tokenId).limit.isNull()) {
                // now check rate limits
                if (_isOutboundAmountRateLimited(tokenId, internalAmount)) {
                    if (!params.args.shouldQueue) {
                        revert NotEnoughCapacity(
                            tokenId, getCurrentOutboundCapacity(tokenId), params.args.amount
                        );
                    }

                    if (params.args.additionalPayload.length != 0) {
                        revert QueuedTransferWithPayload();
                    }

                    uint64 sequence = gmpManager.reserveMessageSequence();

                    emit OutboundTransferRateLimited(
                        msg.sender,
                        sequence,
                        params.args.amount,
                        getCurrentOutboundCapacity(tokenId)
                    );

                    _enqueueOutboundTransfer(
                        sequence,
                        params.args.token,
                        trimmedAmount,
                        params.args.recipientChain,
                        params.args.recipient,
                        params.args.refundAddress,
                        msg.sender,
                        params.args.transceiverInstructions
                    );

                    // refund price quote back to sender
                    _refundToSender(params.msgValue);

                    // return the sequence in the queue
                    return sequence;
                }

                // otherwise, consume the outbound amount
                _consumeOutboundAmount(tokenId, internalAmount);
            }

            // When sending a transfer, we refill the inbound rate limit for
            // that chain by the same amount (we call this "backflow")
            // if the inbound limit is not set, we don't backfill
            // this is to avoid backfilling for chains that don't have an inbound limit set
            if (!_getInboundLimitParamsStorage(tokenId)[params.args.recipientChain].limit.isNull())
            {
                _backfillInboundAmount(tokenId, internalAmount, params.args.recipientChain);
            }
        }

        bytes memory transceiverInstructions = params.args.transceiverInstructions;
        bytes memory additionalPayload = params.args.additionalPayload;

        return _transfer(
            params.msgValue,
            0,
            params.args.token,
            trimmedAmount,
            params.args.recipientChain,
            params.args.recipient,
            params.args.refundAddress,
            msg.sender,
            transceiverInstructions,
            additionalPayload
        );
    }

    function _transfer(
        uint256 msgValue,
        uint64 reservedSequence,
        address token,
        TrimmedAmount amount,
        uint16 recipientChain,
        bytes32 recipient,
        bytes32 refundAddress,
        address sender,
        bytes memory transceiverInstructions,
        bytes memory additionalPayload
    ) internal returns (uint64 msgSequence) {
        // the flow of this code looks interesting here. it's laid out the way
        // it is to avoid stack too deep errors.
        bytes memory message;
        TokenId memory tokenId;
        {
            (tokenId,) = getTokenId(token);
            TokenInfo memory tokenInfo = TokenInfo({meta: _getTokenMeta(tokenId), token: tokenId});

            message = NativeTokenTransferCodec.encodeNativeTokenTransfer(
                NativeTokenTransferCodec.NativeTokenTransfer({
                    amount: amount,
                    token: tokenInfo,
                    sender: toWormholeFormat(sender),
                    to: recipient,
                    additionalPayload: additionalPayload
                })
            );
        }

        msgSequence = _sendMessageWithSequence(
            msgValue,
            recipientChain,
            refundAddress,
            reservedSequence,
            message,
            transceiverInstructions
        );

        uint256 untrimmedAmount = amount.untrim(_tokenDecimals(tokenId));
        emit TransferSent(
            msgSequence,
            tokenId.chainId,
            tokenId.tokenAddress,
            recipient,
            refundAddress,
            untrimmedAmount,
            recipientChain,
            sender
        );
    }

    function _sendMessageWithSequence(
        uint256 msgValue,
        uint16 recipientChain,
        bytes32 refundAddress,
        uint64 reservedSequence,
        bytes memory message,
        bytes memory transceiverInstructions
    ) internal returns (uint64 messageSequence) {
        bytes32 peerAddress = _getPeersStorage()[recipientChain].peerAddress;
        if (peerAddress == bytes32(0)) {
            revert InvalidPeerZeroAddress();
        }
        // _sendMessage invokes the GmpManager contract which takes the payment
        // for sending the message (including paying the transceivers).
        // It then refunds any excess back to this contract, which we refund to
        // the sender of this transaction.
        uint256 balanceBefore = address(this).balance;
        messageSequence = _sendMessage(
            msgValue,
            recipientChain,
            peerAddress,
            refundAddress,
            reservedSequence,
            message,
            transceiverInstructions
        );
        // refund the excess payment. in theory, a transceiver might send back
        // more than we paid it, so our final balance could be higher than before.
        // we use saturing subtraction to handle that case.
        uint256 paid =
            balanceBefore >= address(this).balance ? balanceBefore - address(this).balance : 0;
        if (paid < msgValue) {
            _refundToSender(msgValue - paid);
        }
    }

    // Returns 0 if the token is not yet created
    function getToken(
        TokenId memory tokenId
    ) public view returns (address) {
        if (tokenId.chainId == chainId) {
            return fromWormholeFormat(tokenId.tokenAddress);
        }

        return _getLocalTokenStorage()[tokenId.chainId][tokenId.tokenAddress].token;
    }

    // This function resolves a TokenInfo into a local token address.
    // If the token is actually native to this chain, it will return the address.
    // If the token is foreign, then it will return the local representation of
    // the token, creating it if necessary.
    function _getOrCreateToken(
        TokenInfo memory tokenInfo
    ) internal returns (address) {
        address localToken = getToken(tokenInfo.token);

        if (localToken == address(0)) {
            // create the local token
            localToken = _createLocalToken(tokenInfo);
            _getLocalTokenStorage()[tokenInfo.token.chainId][tokenInfo.token.tokenAddress] =
                LocalTokenInfo({token: localToken});
            _getForeignTokenStorage()[localToken] = tokenInfo.token;
        }

        return localToken;
    }

    // TODO: decide the exact layout of the tokens. should it be a beacon proxy?
    // regular proxy? non-upgradeable?
    function _createLocalToken(
        TokenInfo memory tokenInfo
    ) internal returns (address) {
        return TokenDeployment.createToken(tokenInfo, tokenImplementation);
    }

    function _mintOrUnlockToRecipient(
        bytes32 digest,
        address token,
        address recipient,
        TrimmedAmount amount,
        bool cancelled,
        bytes memory additionalPayload,
        uint16 sourceChainId,
        bytes32 sourceAddress
    ) internal {
        // calculate proper amount of tokens to unlock/mint to recipient
        // untrim the amount
        (TokenId memory tokenId, Mode mode) = getTokenId(token);

        uint256 untrimmedAmount = amount.untrim(_tokenDecimals(tokenId));

        if (cancelled) {
            emit OutboundTransferCancelled(uint256(digest), recipient, untrimmedAmount);
        } else {
            emit TransferRedeemed(digest);
        }

        if (mode == Mode.LOCKING) {
            if (token == address(WETH)) {
                IWETH(token).withdraw(untrimmedAmount);
                (bool success,) = address(recipient).call{value: untrimmedAmount}("");
                if (!success) revert TransferFailed();
            } else {
                // unlock tokens to the specified recipient
                IERC20(token).safeTransfer(recipient, untrimmedAmount);
            }
        } else if (mode == Mode.BURNING) {
            // mint tokens to the specified recipient
            INttToken(token).mint(recipient, untrimmedAmount);
        } else {
            revert(); // impossible
        }

        // If there's additional payload, call the callback
        if (additionalPayload.length > 0) {
            try INttTokenReceiver(recipient).onNttTokenReceived(
                token, untrimmedAmount, additionalPayload, sourceChainId, sourceAddress
            ) {
                // Callback succeeded
            } catch {
                revert NttTokenReceiverCallFailed(recipient);
            }
        }
    }

    bytes private constant symbolSig = abi.encodeWithSignature("symbol()");
    bytes private constant nameSig = abi.encodeWithSignature("name()");
    bytes private constant decimalsSig = abi.encodeWithSignature("decimals()");

    function _queryTokenMetaFromTokenContract(
        address token
    ) internal view returns (TokenMeta memory meta) {
        bytes memory queryResult;

        queryResult = _staticQuery(token, symbolSig);

        string memory symbol = abi.decode(queryResult, (string));
        bytes32 symbol32;
        assembly {
            symbol32 := mload(add(symbol, 32))
        }

        queryResult = _staticQuery(token, nameSig);

        string memory name = abi.decode(queryResult, (string));
        bytes32 name32;
        assembly {
            name32 := mload(add(name, 32))
        }

        queryResult = _staticQuery(token, decimalsSig);

        uint8 decimals = abi.decode(queryResult, (uint8));

        meta.name = name32;
        meta.symbol = symbol32;
        meta.decimals = decimals;
    }

    function _getTokenMeta(
        TokenId memory tokenId
    ) internal view returns (TokenMeta memory meta) {
        address token = _localTokenAddress(tokenId);
        return _queryTokenMetaFromTokenContract(token);
    }

    function _tokenDecimals(
        TokenId memory tokenId
    ) internal view override(MultiTokenRateLimiter) returns (uint8) {
        address token = _localTokenAddress(tokenId);
        bytes memory queriedDecimals = _staticQuery(token, decimalsSig);
        return abi.decode(queriedDecimals, (uint8));
    }

    function _localTokenAddress(TokenId memory tokenId) internal view returns (address) {
        if (tokenId.chainId == chainId) {
            return fromWormholeFormat(tokenId.tokenAddress);
        } else {
            LocalTokenInfo storage localTokenInfo =
                _getLocalTokenStorage()[tokenId.chainId][tokenId.tokenAddress];
            address result = localTokenInfo.token;
            if (result == address(0)) {
                revert TokenNotRegistered(tokenId.chainId, tokenId.tokenAddress);
            }
            return result;
        }
    }

    function _staticQuery(address token, bytes memory sig) internal view returns (bytes memory) {
        (bool success, bytes memory result) = token.staticcall(sig);
        if (!success) {
            revert StaticcallFailed();
        }
        return result;
    }

    // ==================== Internal Helpers ===============================================

    function _trimTransferAmount(
        uint8 toDecimals,
        uint256 amount
    ) internal pure returns (TrimmedAmount) {
        if (toDecimals == 0) {
            // TODO: can this happen? if so, better error message
            revert();
        }

        TrimmedAmount trimmedAmount;
        {
            trimmedAmount = amount.trim(toDecimals, toDecimals); // TODO: we could improve the readability of this.
            // don't deposit dust that can not be bridged due to the decimal shift
            uint256 newAmount = trimmedAmount.untrim(toDecimals);
            if (amount != newAmount) {
                revert TransferAmountHasDust(amount, amount - newAmount);
            }
        }

        return trimmedAmount;
    }

    function _getTokenBalanceOf(
        address tokenAddr,
        address accountAddr
    ) internal view returns (uint256) {
        (bool success, bytes memory queriedBalance) =
            tokenAddr.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, accountAddr));

        if (!success) {
            revert StaticcallFailed();
        }

        return abi.decode(queriedBalance, (uint256));
    }

    // =============== Pause Management ==============================================================

    /// @notice Pauses the contract, blocking all transfer and execution functions
    /// @dev Can be called by owner or pauser
    function pause() public onlyOwnerOrPauser {
        _pause();
    }

    /// @notice Unpauses the contract, re-enabling all functions
    /// @dev Can only be called by owner (not pauser)
    function unpause() public onlyOwner {
        _unpause();
    }
}
"
    },
    "src/GmpManager/GmpManager.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "../interfaces/IGmpManager.sol";
import "../libraries/GmpStructs.sol";
import "./GmpIntegration.sol";
import {ManagerBase} from "../NttManager/ManagerBase.sol";
import "wormhole-solidity-sdk/Utils.sol";

contract GmpManager is IGmpManager, ManagerBase {
    string public constant GMP_MANAGER_VERSION = "1.0.0";

    constructor(
        uint16 _chainId
    ) ManagerBase(_chainId) {}

    struct GmpPeer {
        bytes32 peerAddress;
    }

    function __GmpManager_init() internal onlyInitializing {
        // check if the owner is the deployer of this contract
        if (msg.sender != deployer) {
            revert UnexpectedDeployer(deployer, msg.sender);
        }
        if (msg.value != 0) {
            revert UnexpectedMsgValue();
        }
        __PausedOwnable_init(msg.sender, msg.sender);
        __ReentrancyGuard_init();
        // NOTE: we bump the message counter to start from 1
        // this is so we can use '0' as a sentinel value for unreserved sequences
        _useMessageSequence();
    }

    function _initialize() internal virtual override {
        super._initialize();
        __GmpManager_init();
        // Note: _checkThresholdInvariants() removed since we don't maintain global thresholds
        _checkTransceiversInvariants();
    }

    // =============== Storage ==============================================================

    bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("gmp.peers")) - 1);
    bytes32 private constant RESERVED_SEQUENCES_SLOT =
        bytes32(uint256(keccak256("gmp.reservedSequences")) - 1);

    // =============== Storage Getters/Setters ==============================================

    function _getPeersStorage() internal pure returns (mapping(uint16 => GmpPeer) storage $) {
        uint256 slot = uint256(PEERS_SLOT);
        assembly ("memory-safe") {
            $.slot := slot
        }
    }

    function _getReservedSequencesStorage()
        internal
        pure
        returns (mapping(uint64 => address) storage $)
    {
        uint256 slot = uint256(RESERVED_SEQUENCES_SLOT);
        assembly ("memory-safe") {
            $.slot := slot
        }
    }

    // =============== Public Getters ========================================================

    function getPeer(
        uint16 chainId_
    ) external view returns (GmpPeer memory) {
        return _getPeersStorage()[chainId_];
    }

    function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view override {
        if (sourceChainId == 0) {
            revert InvalidPeerChainIdZero();
        }
        if (peerAddress == bytes32(0)) {
            revert InvalidPeerZeroAddress();
        }
        if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) {
            revert InvalidPeer(sourceChainId, peerAddress);
        }
    }

    // =============== External Interface =========================================================

    /**
     * @notice Reserve a message sequence number for later use
     * @dev This function allows users to reserve sequence numbers ahead of time,
     *      which can be useful for applications that need deterministic sequence numbers
     *      or want to guarantee a specific ordering of messages.
     *
     * @dev Sequence numbers start at 1 (not 0) to allow 0 to be used as a sentinel value
     *      to indicate "no reserved sequence" in sendMessage calls.
     *
     * @dev Reserved sequences are per-sender, meaning only the address that reserved
     *      a sequence can use it in a subsequent sendMessage call.
     *
     * @dev Reserved sequences are single-use - once consumed in a sendMessage call,
     *      they cannot be reused.
     *
     * @return sequence The reserved sequence number
     */
    function reserveMessageSequence() external override returns (uint64 sequence) {
        sequence = _useMessageSequence();
        _getReservedSequencesStorage()[sequence] = msg.sender;
    }

    function _verifyAndConsumeReservedMessageSequence(
        uint64 sequence
    ) internal {
        if (_getReservedSequencesStorage()[sequence] != msg.sender) {
            revert SequenceNotReservedBySender(sequence, msg.sender);
        }
        _getReservedSequencesStorage()[sequence] = address(0);
    }

    // this exists just to minimise the number of local variable assignments :(
    struct PreparedTransfer {
        address[] enabledTransceivers;
        TransceiverStructs.TransceiverInstruction[] instructions;
        uint256[] priceQuotes;
        uint256 totalPriceQuote;
    }

    /**
     * @notice Send a cross-chain message to a target contract
     * @dev This function supports both immediate sequence allocation and pre-reserved sequences.
     *
     * ## Sequence Reservation Flow:
     *
     * ### Option 1: Immediate Sequence Allocation
     * - Pass `reservedSequence = 0` to allocate a new sequence immediately
     * - The function will automatically assign the next available sequence number
     * - This is the default behavior for most use cases
     *
     * ### Option 2: Pre-Reserved Sequence Usage
     * - First call `reserveMessageSequence()` to obtain a reserved sequence number
     * - Then pass that sequence number as `reservedSequence` parameter
     * - The function will validate that the sequence was reserved by the caller
     * - Once used, the reserved sequence is consumed and cannot be reused
     *
     * @param targetChain The Wormhole chain ID of the target chain
     * @param callee The address of the contract to call on the target chain (32-byte format)
     * @param refundAddress The address to refund excess fees to on the target chain (32-byte format)
     * @param reservedSequence The pre-reserved sequence to use, or 0 for immediate allocation
     * @param data The calldata to execute on the target chain
     * @param transceiverInstructions Instructions for transceivers (e.g., gas limits, relayer settings)
     *
     * @return actualSequence The sequence number assigned to this message
     *
     */
    function sendMessage(
        uint16 targetChain,
        bytes32 callee,
        bytes32 refundAddress,
        uint64 reservedSequence,
        bytes calldata data,
        bytes calldata transceiverInstructions
    ) external payable override nonReentrant whenNotPaused returns (uint64 actualSequence) {
        return _sendMessage(
            targetChain, callee, refundAddress, reservedSequence, data, transceiverInstructions
        );
    }

    function _sendMessage(
        uint16 targetChain,
        bytes32 callee,
        bytes32 refundAddress,
        uint64 reservedSequence,
        bytes calldata data,
        bytes calldata transceiverInstructions
    ) internal returns (uint64 sequence) {
        if (callee == bytes32(0)) {
            revert InvalidCallee();
        }

        if (refundAddress == bytes32(0)) {
            revert InvalidRefundAddress();
        }

        // Handle sequence allocation/reservation
        if (reservedSequence == 0) {
            // No sequence provided, allocate a new one
            sequence = _useMessageSequence();
        } else {
            _verifyAndConsumeReservedMessageSequence(reservedSequence);
            sequence = reservedSequence;
        }

        bytes memory encodedGmpManagerPayload;

        {
            GmpStructs.GenericMessage memory message = GmpStructs.GenericMessage({
                toChain: targetChain,
                callee: callee,
                sender: toWormholeFormat(msg.sender),
                data: data
            });

            encodedGmpManagerPayload = TransceiverStructs.encodeNttManagerMessage(
                TransceiverStructs.NttManagerMessage(
                    bytes32(uint256(sequence)),
                    toWormholeFormat(msg.sender),
                    GmpStructs.encodeGenericMessage(message)
                )
            );
        }

        PreparedTransfer memory preparedTransfer;
        {
            (
                address[] memory enabledTransceivers,
                TransceiverStructs.TransceiverInstruction[] memory instructions,
                uint256[] memory priceQuotes,
                uint256 totalPriceQuote
            ) = _prepareForTransfer(targetChain, transceiverInstructions);

            preparedTransfer = PreparedTransfer({
                enabledTransceivers: enabledTransceivers,
                instructions: instructions,
                priceQuotes: priceQuotes,
                totalPriceQuote: totalPriceQuote
            });
        }

        bytes32 peerAddress = _getPeersStorage()[targetChain].peerAddress;
        if (peerAddress == bytes32(0)) {
            revert InvalidPeer(targetChain, peerAddress);
        }

        _sendMessageToTransceivers(
            targetChain,
            refundAddress,
            peerAddress,
            preparedTransfer.priceQuotes,
            preparedTransfer.instructions,
            preparedTransfer.enabledTransceivers,
            encodedGmpManagerPayload
        );

        emit MessageSent(
            sequence, msg.sender, targetChain, callee, data, preparedTransfer.totalPriceQuote
        );
    }

    function executeMsg(
        uint16 sourceChainId,
        bytes32 sourceGmpManagerAddress,
        TransceiverStructs.NttManagerMessage memory message
    ) public override nonReentrant whenNotPaused {
        (bytes32 digest, bool alreadyExecuted) =
            _isMessageExecuted(sourceChainId, sourceGmpManagerAddress, message);

        if (alreadyExecuted) {
            return;
        }

        GmpStructs.GenericMessage memory gmp = GmpStructs.parseGenericMessage(message.payload);

        if (gmp.toChain != chainId) {
            revert InvalidTargetChain(gmp.toChain, chainId);
        }

        address callee = fromWormholeFormat(gmp.callee);
        GmpIntegration(callee).receiveMessage(digest, sourceChainId, gmp.sender, gmp.data);

        emit MessageExecuted(digest, sourceChainId, message.sender, callee, gmp.data);
    }

    // =============== Admin ==============================================================

    function setPeer(uint16 peerChainId, bytes32 peerAddress) public onlyOwner {
        if (peerChainId == 0) {
            revert InvalidPeerChainIdZero();
        }
        if (peerAddress == bytes32(0)) {
            revert InvalidPeerZeroAddress();
        }
        if (peerChainId == chainId) {
            revert InvalidPeerSameChainId();
        }

        GmpPeer memory oldPeer = _getPeersStorage()[peerChainId];

        _getPeersStorage()[peerChainId].peerAddress = peerAddress;

        _addToKnownChains(peerChainId);

        emit PeerUpdated(peerChainId, oldPeer.peerAddress, peerAddress);
    }
}
"
    },
    "src/GmpManager/GmpIntegration.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "../interfaces/IGmpManager.sol";

abstract contract GmpIntegration {
    IGmpManager public immutable gmpManager;

    error OnlyGmpManagerAllowed();

    constructor(
        IGmpManager _gmpManager
    ) {
        gmpManager = _gmpManager;
    }

    modifier onlyGmpManager() {
        if (msg.sender != address(gmpManager)) revert OnlyGmpManagerAllowed();
        _;
    }

    /// @notice Receive a message via the GMP manager.
    /// @dev The GMP manager performs verification and replay protection.
    /// @dev `data` is the payload of the message, which is not necessarily
    ///       unique. `digest` is a unique identifier for the message, which commits
    ///       to metadata not directly included in `data`.
    ///       When an integrator wants to uniquely identify a message, they should
    ///       use `digest` instead of `data`.
    function receiveMessage(
        bytes32 digest,
        uint16 sourceChainId,
        bytes32 sender,
        bytes calldata data
    ) external onlyGmpManager {
        _receiveMessage(digest, sourceChainId, sender, data);
    }

    function _receiveMessage(
        bytes32 digest,
        uint16 sourceChainId,
        bytes32 sender,
        bytes calldata data
    ) internal virtual;

    function _sendMessage(
        uint256 msgValue,
        uint16 targetChain,
        bytes32 callee,
        bytes32 refundAddress,
        uint64 reservedSequence,
        bytes memory data,
        bytes memory transceiverInstructions
    ) internal returns (uint64 sequence) {
        return gmpManager.sendMessage{value: msgValue}(
            targetChain, callee, refundAddress, reservedSequence, data, transceiverInstructions
        );
    }
}
"
    },
    "src/libraries/MultiTokenRateLimiter.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "../interfaces/IMultiTokenRateLimiter.sol";
import "./TokenId.sol";

import "../interfaces/IRateLimiterEvents.sol";
import "./RateLimitLib.sol";
import "./RateLimitAdmin.sol";
import "./TransceiverHelpers.sol";
import "./TransceiverStructs.sol";
import "./TrimmedAmount.sol";

abstract contract MultiTokenRateLimiter is IMultiTokenRateLimiter, IRateLimiterEvents {
    using TrimmedAmountLib for TrimmedAmount;
    using RateLimitLib for RateLimitLib.RateLimitParams;

    /// @dev The duration (in seconds) it takes for the limits to fully replenish.
    uint64 public immutable rateLimitDuration;

    // Constants for storage slots
    bytes32 private constant OUTBOUND_LIMIT_PARAMS_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.outboundLimitParams")) - 1);

    // TODO: maybe the queue should only store a commitment to the queue entry?
    // harder to pick up the data (maybe look at logs), but cheaper
    bytes32 private constant OUTBOUND_QUEUE_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.outboundQueue")) - 1);

    bytes32 private constant INBOUND_LIMIT_PARAMS_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.inboundLimitParams")) - 1);

    bytes32 private constant INBOUND_QUEUE_SLOT =
        bytes32(uint256(keccak256("ntt.multitoken.inboundQueue")) - 1);

    constructor(uint64 _rateLimitDuration, bool _skipRateLimiting) {
        if (
            _rateLimitDuration == 0 && !_skipRateLimiting
                || _rateLimitDuration != 0 && _skipRateLimiting
        ) {
            revert UndefinedRateLimiting();
        }

        rateLimitDuration = _rateLimitDuration;
    }

    function _getOutboundLimitParamsStorage(
        TokenId memory tokenId
    ) internal pure returns (RateLimitLib.RateLimitParams storage $) {
        // TODO: are these xors safe? they should be, but maybe better to introduce a domain separator?
        bytes32 slot =
            bytes32(uint256(OUTBOUND_LIMIT_PARAMS_SLOT) ^ uint256(keccak256(abi.encode(tokenId))));
        assembly {
            $.slot := slot
        }
    }

    function _getOutboundQueueStorage()
        internal
        pure
        returns (mapping(uint64 => OutboundQueuedTransfer) storage $)
    {
        uint256 slot = uint256(OUTBOUND_QUEUE_SLOT);
        assembly {
            $.slot := slot
        }
    }

    function _getInboundLimitParamsStorage(
        TokenId memory tokenId
    ) internal pure returns (mapping(uint16 => RateLimitLib.RateLimitParams) storage $) {
        bytes32 slot =
            bytes32(uint256(INBOUND_LIMIT_PARAMS_SLOT) ^ uint256(keccak256(abi.encode(tokenId))));
        assembly {
            $.slot := slot
        }
    }

    function _getInboundQueueStorage()
        internal
        pure
        returns (mapping(bytes32 => InboundQueuedTransfer) storage $)
    {
        uint256 slot = uint256(INBOUND_QUEUE_SLOT);
        assembly {
            $.slot := slot
        }
    }

    function _setOutboundLimit(TokenId memory tokenId, TrimmedAmount limit) internal {
        RateLimitAdmin.setLimit(_getOutboundLimitParamsStorage(tokenId), limit, rateLimitDuration);
    }

    function getOutboundLimitParams(
        TokenId memory tokenId
    ) public pure returns (RateLimitLib.RateLimitParams memory) {
        return _getOutboundLimitParamsStorage(tokenId);
    }

    function getCurrentOutboundCapacity(
        TokenId memory tokenId
    ) public view override returns (uint256) {
        RateLimitLib.RateLimitParams storage params = _getOutboundLimitParamsStorage(tokenId);
        TrimmedAmount trimmedCapacity = params.getCurrentCapacity(rateLimitDuration);
        uint8 decimals = _tokenDecimals(tokenId);
        return trimmedCapacity.untrim(decimals);
    }

    function getOutboundQueuedTransfer(
        uint64 queueSequence
    ) public view override returns (OutboundQueuedTransfer memory) {
        return _getOutboundQueueStorage()[queueSequence];
    }

    function _setInboundLimit(
        TokenId memory tokenId,
        TrimmedAmount limit,
        uint16 chainId_
    ) internal {
        RateLimitAdmin.setLimit(
            _getInboundLimitParamsStorage(tokenId)[chainId_], limit, rateLimitDuration
        );
    }

    function getInboundLimitParams(
        TokenId memory tokenId,
        uint16 chainId_
    ) public view returns (RateLimitLib.RateLimitParams memory) {
        return _getInboundLimitParamsStorage(tokenId)[chainId_];
    }

    function getCurrentInboundCapacity(
        TokenId memory tokenId,
        uint16 chainId_
    ) public view override returns (uint256) {
        RateLimitLib.RateLimitParams storage params =
            _getInboundLimitParamsStorage(tokenId)[chainId_];
        TrimmedAmount trimmedCapacity = params.getCurrentCapacity(rateLimitDuration);
        uint8 decimals = _tokenDecimals(tokenId);
        return trimmedCapacity.untrim(decimals);
    }

    function getInboundQueuedTransfer(
        bytes32 digest
    ) public view override returns (InboundQueuedTransfer memory) {
        return _getInboundQueueStorage()[digest];
    }

    function _consumeOutboundAmount(TokenId memory tokenId, TrimmedAmount amount) internal {
        if (rateLimitDuration == 0) return;
        _getOutboundLimitParamsStorage(tokenId).consumeAmount(amount, rateLimitDuration);
    }

    function _backfillOutboundAmount(TokenId memory tokenId, TrimmedAmount amount) internal {
        if (rateLimitDuration == 0) return;
        _getOutboundLimitParamsStorage(tokenId).backfillAmount(amount, rateLimitDuration);
    }

    function _consumeInboundAmount(
        TokenId memory tokenId,
        TrimmedAmount amount,
        uint16 chainId_
    ) internal {
        if (rateLimitDuration == 0) return;
        _getInboundLimitParamsStorage(tokenId)[chainId_].consumeAmount(amount, rateLimitDuration);
    }

    function _backfillInboundAmount(
        TokenId memory tokenId,
        TrimmedAmount amount,
        uint16 chainId_
    ) internal {
        if (rateLimitDuration == 0) return;
        _getInboundLimitParamsStorage(tokenId)[chainId_].backfillAmount(amount, rateLimitDuration);
    }

    function _isOutboundAmountRateLimited(
        TokenId memory tokenId,
        TrimmedAmount amount
    ) internal view returns (bool) {
        if (rateLimitDuration == 0) return false;
        return
            _getOutboundLimitParamsStorage(tokenId).isAmountRateLimited(amount, rateLimitDuration);
    }

    function _isInboundAmountRateLimited(
        TokenId memory tokenId,
        TrimmedAmount amount,
        uint16 chainId_
    ) internal view returns (bool) {
        if (rateLimitDuration == 0) return false;
        return _getInboundLimitParamsStorage(tokenId)[chainId_].isAmountRateLimited(
            amount, rateLimitDuration
        );
    }

    function _enqueueOutboundTransfer(
        uint64 sequence,
        address token,
        TrimmedAmount amount,
        uint16 recipientChain,
        bytes32 recipient,
        bytes32 refundAddress,
        address senderAddress,
        bytes memory transceiverInstructions
    ) internal {
        _getOutboundQueueStorage()[sequence] = OutboundQueuedTransfer({
            amount: amount,
            recipientChain: recipientChain,
            recipient: recipient,
            refundAddress: refundAddress,
            txTimestamp: uint64(block.timestamp),
            sender: senderAddress,
            token: token,
            transceiverInstructions: transceiverInstructions
        });

        emit OutboundTransferQueued(sequence);
    }

    function _enqueueInboundTransfer(
        bytes32 digest,
        uint16 sourceChainId,
        bytes20 transferDigest
    ) internal {
        _getInboundQueueStorage()[digest] = InboundQueuedTransfer({
            txTimestamp: uint64(block.timestamp),
            sourceChainId: sourceChainId,
            transferDigest: transferDigest
        });

        emit InboundTransferQueued(digest);
    }

    function _tokenDecimals(
        TokenId memory tokenId
    ) internal view virtual returns (uint8);
}
"
    },
    "src/libraries/TokenId.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

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

struct TokenId {
    // TODO: swapping these around might make it easier to pack the chainId into
    // adjacent slots. need to review the use sites.
    uint16 chainId;
    bytes32 tokenAddress;
}

library TokenIdLib {
    using BytesParsing for bytes;

    function encode(
        TokenId memory tokenId
    ) internal pure returns (bytes memory encoded) {
        encoded = abi.encodePacked(tokenId.chainId, tokenId.tokenAddress);
    }

    function asTokenIdUnchecked(
        bytes memory encoded,
        uint256 offset
    ) internal pure returns (TokenId memory tokenId, uint256 newOffset) {
        (tokenId.chainId, offset) = encoded.asUint16Unchecked(offset);
        (tokenId.tokenAddress, offset) = encoded.asBytes32Unchecked(offset);
        newOffset = offset;
    }
}
"
    },
    "src/libraries/TokenMeta.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

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

struct TokenMeta {
    bytes32 name;
    bytes32 symbol;
    uint8 decimals;
}

library TokenMetaLib {
    using BytesParsing for bytes;

    function encode(
        TokenMeta memory m
    ) internal pure returns (bytes memory encoded) {
        return abi.encodePacked(m.name, m.symbol, m.decimals);
    }

    function asTokenMetaUnchecked(
        bytes memory encoded,
        uint256 offset
    ) internal pure returns (TokenMeta memory tokenMeta, uint256 newOffset) {
        (tokenMeta.name, offset) = encoded.asBytes32Unchecked(offset);
        (tokenMeta.symbol, offset) = encoded.asBytes32Unchecked(offset);
        (tokenMeta.decimals, offset) = encoded.asUint8Unchecked(offset);
        newOffset = offset;
    }
}
"
    },
    "src/libraries/TokenInfo.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "./TokenId.sol";
import "./TokenMeta.sol";

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

// A token is either
//
// a) local
// b) foreign
//
// If it's foreign, then it has a local representation token

struct TokenInfo {
    TokenMeta meta;
    TokenId token;
}

library TokenInfoLib {
    using BytesParsing for bytes;
    using TokenMetaLib for TokenMeta;
    using TokenMetaLib for bytes;
    using TokenIdLib for TokenId;
    using TokenIdLib for bytes;

    function encode(
        TokenInfo memory m
    ) internal pure returns (bytes memory encoded) {
        return abi.encodePacked(m.meta.encode(), m.token.encode());
    }

    function asTokenInfoUnchecked(
        bytes memory encoded,
        uint256 offset
    ) internal pure returns (TokenInfo memory tokenInfo, uint256 newOffset) {
        (tokenInfo.meta, offset) = encoded.asTokenMetaUnchecked(offset);
        (tokenInfo.token, offset) = encoded.asTokenIdUnchecked(offset);
        newOffset = offset;
    }
}
"
    },
    "src/libraries/NativeTokenTransferCodec.sol": {
      "content": "// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

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

/// @title NativeTokenTransferCodec
/// @notice Library for encoding and decoding native token transfer messages in the multi-token NTT system
library NativeTokenTransferCodec {
    using TrimmedAmountLib for uint256;
    using TrimmedAmountLib for TrimmedAmount;
    using BytesParsing for bytes;
    using TokenIdLib for TokenId;
    using TokenIdLib for bytes;
    using TokenMetaLib for TokenMeta;
    using TokenMetaLib for bytes;
    using TokenInfoLib for TokenInfo;
    using TokenInfoLib for bytes;

    error IncorrectPrefix(bytes4 prefix);
    error PayloadTooLong(uint256 length);

    struct NativeTokenTransfer {
        TrimmedAmount amount;
        TokenInfo token;
        bytes32 sender;
        bytes32 to;
        bytes additionalPayload;
    }

    /// @dev Prefix for all NativeTokenTransfer payloads
    ///      This is 0x99'M''T''T'
    bytes4 constant MTT_PREFIX = 0x994D5454;

    function encodeNativeTokenTransfer(
        NativeTokenTransfer memory m
    ) internal pure returns (bytes memory encoded) {
        TrimmedAmount transferAmount = m.amount;

        // Always include payload length prefix for consistent wire format
        if (m.additionalPayload.length > type(uint16).max) {
            revert PayloadTooLong(m.additionalPayload.length);
        }
        uint16 additionalPayloadLength = uint16(m.additionalPayload.length);

        return abi.encodePacked(
            MTT_PREFIX,
            transferAmount.getDecimals(),
            transferAmount.getAmount(),
            m.token.encode(),
            m.sender,
            m.to,
            additionalPayloadLength,
            m.additionalPayload
        );
    }

    function parseNativeTokenTransfer(
        bytes memory encoded
    ) internal pure returns (NativeTokenTransfer memory m) {
        uint256 offset = 0;
        bytes4 prefix;
        (prefix, offset) = encoded.asBytes4Unchecked(offset);
        if (prefix != MTT_PREFIX) {
            revert IncorrectPrefix(prefix);
        }

        uint8 decimals;
        (decimals, offset) = encoded.asUint8Unchecked(offset);
        uint64 amount;
        (amount, offset) = encoded.asUint64Unchecked(offset);
        m.amount = packTrimmedAmount(amount, decimals);

        (m.token, offset) = encoded.asTokenInfoUnchecked(offset);
        (m.sender, offset) = encoded.asBytes32Unchecked(offset);
        (m.to, 

Tags:
ERC20, Multisig, Mintable, Burnable, Pausable, Voting, Upgradeable, Multi-Signature, Factory|addr:0xbd62568e7f561f2067d6916d9cbd6217b4f46127|verified:true|block:23721716|tx:0xe1b120254f0f22ed00bdd3d496aed31c18f6738766b9da0186b904add3e5f7fa|first_check:1762246677

Submitted on: 2025-11-04 09:57:59

Comments

Log in to comment.

No comments yet.