// File: src/gateway/Gateway.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.21;

import {Messages} from "./Messages.sol";
import {Auth} from "./../util/Auth.sol";

interface InvestmentManagerLike {
    function handleExecutedDecreaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 remainingInvestOrder
    ) external;
    function handleExecutedDecreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 trancheTokensPayout,
        uint128 remainingRedeemOrder
    ) external;
    function handleExecutedCollectInvest(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 trancheTokensPayout,
        uint128 remainingInvestOrder
    ) external;
    function handleExecutedCollectRedeem(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 trancheTokensPayout,
        uint128 remainingRedeemOrder
    ) external;
    function handleTriggerIncreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 trancheTokenAmount
    ) external;
}

interface PoolManagerLike {
    function addPool(uint64 poolId) external;
    function allowInvestmentCurrency(uint64 poolId, uint128 currency) external;
    function disallowInvestmentCurrency(uint64 poolId, uint128 currency) external;
    function addTranche(
        uint64 poolId,
        bytes16 trancheId,
        string memory tokenName,
        string memory tokenSymbol,
        uint8 decimals,
        uint8 restrictionSet
    ) external;
    function updateMember(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) external;
    function freeze(uint64 poolId, bytes16 trancheId, address user) external;
    function unfreeze(uint64 poolId, bytes16 trancheId, address user) external;
    function updateTrancheTokenMetadata(
        uint64 poolId,
        bytes16 trancheId,
        string memory tokenName,
        string memory tokenSymbol
    ) external;
    function updateTrancheTokenPrice(
        uint64 poolId,
        bytes16 trancheId,
        uint128 currencyId,
        uint128 price,
        uint64 computedAt
    ) external;
    function addCurrency(uint128 currency, address currencyAddress) external;
    function handleTransfer(uint128 currency, address recipient, uint128 amount) external;
    function handleTransferTrancheTokens(uint64 poolId, bytes16 trancheId, address destinationAddress, uint128 amount)
        external;
}

interface RouterLike {
    function send(bytes memory message) external;
}

interface RootLike {
    function paused() external returns (bool);
    function scheduleRely(address target) external;
    function cancelRely(address target) external;
}

/// @title  Gateway
/// @dev    It parses incoming messages and forwards these to Managers
///         and it encoded outgoing messages and sends these to Routers.
///
///         If the Root is paused, any messages in and out of this contract
///         will not be forwarded
contract Gateway is Auth {
    RootLike public immutable root;
    PoolManagerLike public poolManager;
    InvestmentManagerLike public investmentManager;

    RouterLike public outgoingRouter;
    mapping(address => bool) public incomingRouters;

    // --- Events ---
    event File(bytes32 indexed what, address data);
    event AddIncomingRouter(address indexed router);
    event RemoveIncomingRouter(address indexed router);
    event UpdateOutgoingRouter(address indexed router);

    constructor(address root_, address investmentManager_, address poolManager_, address router_) {
        root = RootLike(root_);
        investmentManager = InvestmentManagerLike(investmentManager_);
        poolManager = PoolManagerLike(poolManager_);
        incomingRouters[router_] = true;
        outgoingRouter = RouterLike(router_);

        wards[msg.sender] = 1;
        emit Rely(msg.sender);
    }

    modifier onlyInvestmentManager() {
        require(msg.sender == address(investmentManager), "Gateway/only-investment-manager-allowed-to-call");
        _;
    }

    modifier onlyPoolManager() {
        require(msg.sender == address(poolManager), "Gateway/only-pool-manager-allowed-to-call");
        _;
    }

    modifier onlyIncomingRouter() {
        require(incomingRouters[msg.sender], "Gateway/only-router-allowed-to-call");
        _;
    }

    modifier pauseable() {
        require(!root.paused(), "Gateway/paused");
        _;
    }

    // --- Administration ---
    function file(bytes32 what, address data) public auth {
        if (what == "poolManager") poolManager = PoolManagerLike(data);
        else if (what == "investmentManager") investmentManager = InvestmentManagerLike(data);
        else revert("Gateway/file-unrecognized-param");
        emit File(what, data);
    }

    function addIncomingRouter(address router) public auth {
        incomingRouters[router] = true;
        emit AddIncomingRouter(router);
    }

    function removeIncomingRouter(address router) public auth {
        incomingRouters[router] = false;
        emit RemoveIncomingRouter(router);
    }

    function updateOutgoingRouter(address router) public auth {
        outgoingRouter = RouterLike(router);
        emit UpdateOutgoingRouter(router);
    }

    // --- Outgoing ---
    function transferTrancheTokensToCentrifuge(
        uint64 poolId,
        bytes16 trancheId,
        address sender,
        bytes32 destinationAddress,
        uint128 amount
    ) public onlyPoolManager pauseable {
        outgoingRouter.send(
            Messages.formatTransferTrancheTokens(
                poolId,
                trancheId,
                _addressToBytes32(sender),
                Messages.formatDomain(Messages.Domain.Centrifuge),
                destinationAddress,
                amount
            )
        );
    }

    function transferTrancheTokensToEVM(
        uint64 poolId,
        bytes16 trancheId,
        address sender,
        uint64 destinationChainId,
        address destinationAddress,
        uint128 amount
    ) public onlyPoolManager pauseable {
        outgoingRouter.send(
            Messages.formatTransferTrancheTokens(
                poolId,
                trancheId,
                _addressToBytes32(sender),
                Messages.formatDomain(Messages.Domain.EVM, destinationChainId),
                destinationAddress,
                amount
            )
        );
    }

    function transfer(uint128 token, address sender, bytes32 receiver, uint128 amount)
        public
        onlyPoolManager
        pauseable
    {
        outgoingRouter.send(Messages.formatTransfer(token, _addressToBytes32(sender), receiver, amount));
    }

    function increaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 currencyAmount
    ) public onlyInvestmentManager pauseable {
        outgoingRouter.send(
            Messages.formatIncreaseInvestOrder(poolId, trancheId, _addressToBytes32(investor), currency, currencyAmount)
        );
    }

    function decreaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 currencyAmount
    ) public onlyInvestmentManager pauseable {
        outgoingRouter.send(
            Messages.formatDecreaseInvestOrder(poolId, trancheId, _addressToBytes32(investor), currency, currencyAmount)
        );
    }

    function increaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 trancheTokenAmount
    ) public onlyInvestmentManager pauseable {
        outgoingRouter.send(
            Messages.formatIncreaseRedeemOrder(
                poolId, trancheId, _addressToBytes32(investor), currency, trancheTokenAmount
            )
        );
    }

    function decreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        address investor,
        uint128 currency,
        uint128 trancheTokenAmount
    ) public onlyInvestmentManager pauseable {
        outgoingRouter.send(
            Messages.formatDecreaseRedeemOrder(
                poolId, trancheId, _addressToBytes32(investor), currency, trancheTokenAmount
            )
        );
    }

    function collectInvest(uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
        public
        onlyInvestmentManager
        pauseable
    {
        outgoingRouter.send(Messages.formatCollectInvest(poolId, trancheId, _addressToBytes32(investor), currency));
    }

    function collectRedeem(uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
        public
        onlyInvestmentManager
        pauseable
    {
        outgoingRouter.send(Messages.formatCollectRedeem(poolId, trancheId, _addressToBytes32(investor), currency));
    }

    function cancelInvestOrder(uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
        public
        onlyInvestmentManager
        pauseable
    {
        outgoingRouter.send(Messages.formatCancelInvestOrder(poolId, trancheId, _addressToBytes32(investor), currency));
    }

    function cancelRedeemOrder(uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
        public
        onlyInvestmentManager
        pauseable
    {
        outgoingRouter.send(Messages.formatCancelRedeemOrder(poolId, trancheId, _addressToBytes32(investor), currency));
    }

    // --- Incoming ---
    function handle(bytes calldata message) external onlyIncomingRouter pauseable {
        if (Messages.isAddCurrency(message)) {
            (uint128 currency, address currencyAddress) = Messages.parseAddCurrency(message);
            poolManager.addCurrency(currency, currencyAddress);
        } else if (Messages.isAddPool(message)) {
            (uint64 poolId) = Messages.parseAddPool(message);
            poolManager.addPool(poolId);
        } else if (Messages.isAllowInvestmentCurrency(message)) {
            (uint64 poolId, uint128 currency) = Messages.parseAllowInvestmentCurrency(message);
            poolManager.allowInvestmentCurrency(poolId, currency);
        } else if (Messages.isAddTranche(message)) {
            (
                uint64 poolId,
                bytes16 trancheId,
                string memory tokenName,
                string memory tokenSymbol,
                uint8 decimals,
                uint8 restrictionSet
            ) = Messages.parseAddTranche(message);
            poolManager.addTranche(poolId, trancheId, tokenName, tokenSymbol, decimals, restrictionSet);
        } else if (Messages.isUpdateMember(message)) {
            (uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) = Messages.parseUpdateMember(message);
            poolManager.updateMember(poolId, trancheId, user, validUntil);
        } else if (Messages.isUpdateTrancheTokenPrice(message)) {
            (uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price, uint64 computedAt) =
                Messages.parseUpdateTrancheTokenPrice(message);
            poolManager.updateTrancheTokenPrice(poolId, trancheId, currencyId, price, computedAt);
        } else if (Messages.isTransfer(message)) {
            (uint128 currency, address recipient, uint128 amount) = Messages.parseIncomingTransfer(message);
            poolManager.handleTransfer(currency, recipient, amount);
        } else if (Messages.isTransferTrancheTokens(message)) {
            (uint64 poolId, bytes16 trancheId, address destinationAddress, uint128 amount) =
                Messages.parseTransferTrancheTokens20(message);
            poolManager.handleTransferTrancheTokens(poolId, trancheId, destinationAddress, amount);
        } else if (Messages.isExecutedDecreaseInvestOrder(message)) {
            (
                uint64 poolId,
                bytes16 trancheId,
                address investor,
                uint128 currency,
                uint128 currencyPayout,
                uint128 remainingInvestOrder
            ) = Messages.parseExecutedDecreaseInvestOrder(message);
            investmentManager.handleExecutedDecreaseInvestOrder(
                poolId, trancheId, investor, currency, currencyPayout, remainingInvestOrder
            );
        } else if (Messages.isExecutedDecreaseRedeemOrder(message)) {
            (
                uint64 poolId,
                bytes16 trancheId,
                address investor,
                uint128 currency,
                uint128 trancheTokensPayout,
                uint128 remainingRedeemOrder
            ) = Messages.parseExecutedDecreaseRedeemOrder(message);
            investmentManager.handleExecutedDecreaseRedeemOrder(
                poolId, trancheId, investor, currency, trancheTokensPayout, remainingRedeemOrder
            );
        } else if (Messages.isExecutedCollectInvest(message)) {
            (
                uint64 poolId,
                bytes16 trancheId,
                address investor,
                uint128 currency,
                uint128 currencyPayout,
                uint128 trancheTokensPayout,
                uint128 remainingInvestOrder
            ) = Messages.parseExecutedCollectInvest(message);
            investmentManager.handleExecutedCollectInvest(
                poolId, trancheId, investor, currency, currencyPayout, trancheTokensPayout, remainingInvestOrder
            );
        } else if (Messages.isExecutedCollectRedeem(message)) {
            (
                uint64 poolId,
                bytes16 trancheId,
                address investor,
                uint128 currency,
                uint128 currencyPayout,
                uint128 trancheTokensPayout,
                uint128 remainingRedeemOrder
            ) = Messages.parseExecutedCollectRedeem(message);
            investmentManager.handleExecutedCollectRedeem(
                poolId, trancheId, investor, currency, currencyPayout, trancheTokensPayout, remainingRedeemOrder
            );
        } else if (Messages.isScheduleUpgrade(message)) {
            address target = Messages.parseScheduleUpgrade(message);
            root.scheduleRely(target);
        } else if (Messages.isCancelUpgrade(message)) {
            address target = Messages.parseCancelUpgrade(message);
            root.cancelRely(target);
        } else if (Messages.isUpdateTrancheTokenMetadata(message)) {
            (uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol) =
                Messages.parseUpdateTrancheTokenMetadata(message);
            poolManager.updateTrancheTokenMetadata(poolId, trancheId, tokenName, tokenSymbol);
        } else if (Messages.isTriggerIncreaseRedeemOrder(message)) {
            (uint64 poolId, bytes16 trancheId, address investor, uint128 currency, uint128 trancheTokenAmount) =
                Messages.parseTriggerIncreaseRedeemOrder(message);
            investmentManager.handleTriggerIncreaseRedeemOrder(
                poolId, trancheId, investor, currency, trancheTokenAmount
            );
        } else if (Messages.isFreeze(message)) {
            (uint64 poolId, bytes16 trancheId, address user) = Messages.parseFreeze(message);
            poolManager.freeze(poolId, trancheId, user);
        } else if (Messages.isUnfreeze(message)) {
            (uint64 poolId, bytes16 trancheId, address user) = Messages.parseUnfreeze(message);
            poolManager.unfreeze(poolId, trancheId, user);
        } else if (Messages.isDisallowInvestmentCurrency(message)) {
            (uint64 poolId, uint128 currency) = Messages.parseDisallowInvestmentCurrency(message);
            poolManager.disallowInvestmentCurrency(poolId, currency);
        } else {
            revert("Gateway/invalid-message");
        }
    }

    // --- Helpers ---
    function _addressToBytes32(address x) internal pure returns (bytes32) {
        return bytes32(bytes20(x));
    }
}

// File: src/gateway/Messages.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.21;

import {BytesLib} from "src/util/BytesLib.sol";

/// @title  Messages
/// @dev    Library for encoding and decoding messages.
library Messages {
    enum Call
    /// 0 - An invalid message
    {
        Invalid,
        /// 1 - Add a currency id -> EVM address mapping
        AddCurrency,
        /// 2 - Add Pool
        AddPool,
        /// 3 - Allow a currency to be used as a currency for investing in pools
        AllowInvestmentCurrency,
        /// 4 - Add a Pool's Tranche Token
        AddTranche,
        /// 5 - Update the price of a Tranche Token
        UpdateTrancheTokenPrice,
        /// 6 - Update the member list of a tranche token with a new member
        UpdateMember,
        /// 7 - A transfer of currency
        Transfer,
        /// 8 - A transfer of tranche tokens
        TransferTrancheTokens,
        /// 9 - Increase an investment order by a given amount
        IncreaseInvestOrder,
        /// 10 - Decrease an investment order by a given amount
        DecreaseInvestOrder,
        /// 11 - Increase a Redeem order by a given amount
        IncreaseRedeemOrder,
        /// 12 - Decrease a Redeem order by a given amount
        DecreaseRedeemOrder,
        /// 13 - Collect investment
        CollectInvest,
        /// 14 - Collect Redeem
        CollectRedeem,
        /// 15 - Executed Decrease Invest Order
        ExecutedDecreaseInvestOrder,
        /// 16 - Executed Decrease Redeem Order
        ExecutedDecreaseRedeemOrder,
        /// 17 - Executed Collect Invest
        ExecutedCollectInvest,
        /// 18 - Executed Collect Redeem
        ExecutedCollectRedeem,
        /// 19 - Cancel an investment order
        CancelInvestOrder,
        /// 20 - Cancel a redeem order
        CancelRedeemOrder,
        /// 21 - Schedule an upgrade contract to be granted admin rights
        ScheduleUpgrade,
        /// 22 - Cancel a previously scheduled upgrade
        CancelUpgrade,
        /// 23 - Update tranche token metadata
        UpdateTrancheTokenMetadata,
        /// 24 - Disallow a currency to be used as a currency for investing in pools
        DisallowInvestmentCurrency,
        /// 25 - Freeze tranche tokens
        Freeze,
        /// 26 - Unfreeze tranche tokens
        Unfreeze,
        /// 27 - Request redeem investor
        TriggerIncreaseRedeemOrder
    }

    enum Domain {
        Centrifuge,
        EVM
    }

    function messageType(bytes memory _msg) internal pure returns (Call _call) {
        _call = Call(BytesLib.toUint8(_msg, 0));
    }

    /**
     * Add Currency
     *
     * 0: call type (uint8 = 1 byte)
     * 1-16: The Liquidity Pool's global currency id (uint128 = 16 bytes)
     * 17-36: The EVM address of the currency (address = 20 bytes)
     */
    function formatAddCurrency(uint128 currency, address currencyAddress) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.AddCurrency), currency, currencyAddress);
    }

    function isAddCurrency(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.AddCurrency;
    }

    function parseAddCurrency(bytes memory _msg) internal pure returns (uint128 currency, address currencyAddress) {
        currency = BytesLib.toUint128(_msg, 1);
        currencyAddress = BytesLib.toAddress(_msg, 17);
    }

    /**
     * Add pool
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     */
    function formatAddPool(uint64 poolId) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.AddPool), poolId);
    }

    function isAddPool(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.AddPool;
    }

    function parseAddPool(bytes memory _msg) internal pure returns (uint64 poolId) {
        poolId = BytesLib.toUint64(_msg, 1);
    }

    /**
     * Allow Investment Currency
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: currency (uint128 = 16 bytes)
     */
    function formatAllowInvestmentCurrency(uint64 poolId, uint128 currency) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.AllowInvestmentCurrency), poolId, currency);
    }

    function isAllowInvestmentCurrency(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.AllowInvestmentCurrency;
    }

    function parseAllowInvestmentCurrency(bytes memory _msg) internal pure returns (uint64 poolId, uint128 currency) {
        poolId = BytesLib.toUint64(_msg, 1);
        currency = BytesLib.toUint128(_msg, 9);
    }

    /**
     * Add tranche
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-152: tokenName (string = 128 bytes)
     * 153-184: tokenSymbol (string = 32 bytes)
     * 185: decimals (uint8 = 1 byte)
     * 186: restriction set (uint8 = 1 byte)
     */
    function formatAddTranche(
        uint64 poolId,
        bytes16 trancheId,
        string memory tokenName,
        string memory tokenSymbol,
        uint8 decimals,
        uint8 restrictionSet
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.AddTranche),
            poolId,
            trancheId,
            _stringToBytes128(tokenName),
            _stringToBytes32(tokenSymbol),
            decimals,
            restrictionSet
        );
    }

    function isAddTranche(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.AddTranche;
    }

    function parseAddTranche(bytes memory _msg)
        internal
        pure
        returns (
            uint64 poolId,
            bytes16 trancheId,
            string memory tokenName,
            string memory tokenSymbol,
            uint8 decimals,
            uint8 restrictionSet
        )
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        tokenName = _bytes128ToString(BytesLib.slice(_msg, 25, 128));
        tokenSymbol = _bytes32ToString(BytesLib.toBytes32(_msg, 153));
        decimals = BytesLib.toUint8(_msg, 185);
        restrictionSet = 0;
        if (_msg.length > 186) {
            restrictionSet = BytesLib.toUint8(_msg, 186);
        }
    }

    /**
     * Update member
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-45: user (Ethereum address, 20 bytes - Skip 12 bytes from 32-byte addresses)
     * 57-65: validUntil (uint64 = 8 bytes)
     *
     */
    function formatUpdateMember(uint64 poolId, bytes16 trancheId, address member, uint64 validUntil)
        internal
        pure
        returns (bytes memory)
    {
        return formatUpdateMember(poolId, trancheId, bytes32(bytes20(member)), validUntil);
    }

    function formatUpdateMember(uint64 poolId, bytes16 trancheId, bytes32 member, uint64 validUntil)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.UpdateMember), poolId, trancheId, member, validUntil);
    }

    function isUpdateMember(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.UpdateMember;
    }

    function parseUpdateMember(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, address user, uint64 validUntil)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        user = BytesLib.toAddress(_msg, 25);
        validUntil = BytesLib.toUint64(_msg, 57);
    }

    /**
     * Update a Tranche token's price
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-40: currency (uint128 = 16 bytes)
     * 41-56: price (uint128 = 16 bytes)
     * 57-64: computedAt (uint64 = 8 bytes)
     */
    function formatUpdateTrancheTokenPrice(
        uint64 poolId,
        bytes16 trancheId,
        uint128 currencyId,
        uint128 price,
        uint64 computedAt
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.UpdateTrancheTokenPrice), poolId, trancheId, currencyId, price, computedAt);
    }

    function isUpdateTrancheTokenPrice(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.UpdateTrancheTokenPrice;
    }

    function parseUpdateTrancheTokenPrice(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price, uint64 computedAt)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        currencyId = BytesLib.toUint128(_msg, 25);
        price = BytesLib.toUint128(_msg, 41);
        computedAt = BytesLib.toUint64(_msg, 57);
    }

    /*
     * Transfer Message - Transfer stable coins
     *
     * 0: call type (uint8 = 1 byte)
     * 1-16: currency (uint128 = 16 bytes)
     * 17-48: sender address (32 bytes)
     * 49-80: receiver address (32 bytes)
     * 81-96: amount (uint128 = 16 bytes)
     */
    function formatTransfer(uint128 currency, bytes32 sender, bytes32 receiver, uint128 amount)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.Transfer), currency, sender, receiver, amount);
    }

    function isTransfer(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.Transfer;
    }

    function parseTransfer(bytes memory _msg)
        internal
        pure
        returns (uint128 currency, bytes32 sender, bytes32 receiver, uint128 amount)
    {
        currency = BytesLib.toUint128(_msg, 1);
        sender = BytesLib.toBytes32(_msg, 17);
        receiver = BytesLib.toBytes32(_msg, 49);
        amount = BytesLib.toUint128(_msg, 81);
    }

    // An optimised `parseTransfer` function that saves gas by ignoring the `sender` field and that
    // parses and returns the `recipient` as an `address` instead of the `bytes32` the message holds.
    function parseIncomingTransfer(bytes memory _msg)
        internal
        pure
        returns (uint128 currency, address recipient, uint128 amount)
    {
        currency = BytesLib.toUint128(_msg, 1);
        recipient = BytesLib.toAddress(_msg, 49);
        amount = BytesLib.toUint128(_msg, 81);
    }

    /**
     * TransferTrancheTokens
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: sender (bytes32)
     * 57-65: destinationDomain ((Domain: u8, ChainId: u64) =  9 bytes total)
     * 66-97: destinationAddress (32 bytes - Either a Centrifuge address or an EVM address followed by 12 zeros)
     * 98-113: amount (uint128 = 16 bytes)
     */
    function formatTransferTrancheTokens(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 sender,
        bytes9 destinationDomain,
        bytes32 destinationAddress,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.TransferTrancheTokens), poolId, trancheId, sender, destinationDomain, destinationAddress, amount
        );
    }

    // Overload: Format a TransferTrancheTokens to an EVM domain
    // Note: This is an overload function to dry the cast from `address` to `bytes32`
    // for the `destinationAddress` field by using the default `formatTransferTrancheTokens` implementation
    // by appending 12 zeros to the evm-based `destinationAddress`.
    function formatTransferTrancheTokens(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 sender,
        bytes9 destinationDomain,
        address destinationAddress,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return formatTransferTrancheTokens(
            poolId, trancheId, sender, destinationDomain, bytes32(bytes20(destinationAddress)), amount
        );
    }

    function isTransferTrancheTokens(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.TransferTrancheTokens;
    }

    // Parse a TransferTrancheTokens to an EVM-based `destinationAddress` (20-byte long).
    // We ignore the `sender` and the `domain` since it's not relevant when parsing an incoming message.
    function parseTransferTrancheTokens20(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, address destinationAddress, uint128 amount)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        // ignore: `sender` at bytes 25-56
        // ignore: `domain` at bytes 57-65
        destinationAddress = BytesLib.toAddress(_msg, 66);
        amount = BytesLib.toUint128(_msg, 98);
    }

    /*
     * IncreaseInvestOrder Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     * 57-72: currency (uint128 = 16 bytes)
     * 73-89: amount (uint128 = 16 bytes)
     */
    function formatIncreaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.IncreaseInvestOrder), poolId, trancheId, investor, currency, amount);
    }

    function isIncreaseInvestOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.IncreaseInvestOrder;
    }

    function parseIncreaseInvestOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency, uint128 amount)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toBytes32(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        amount = BytesLib.toUint128(_msg, 73);
    }

    /*
     * DecreaseInvestOrder Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     * 57-72: currency (uint128 = 16 bytes)
     * 73-89: amount (uint128 = 16 bytes)
     */
    function formatDecreaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.DecreaseInvestOrder), poolId, trancheId, investor, currency, amount);
    }

    function isDecreaseInvestOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.DecreaseInvestOrder;
    }

    function parseDecreaseInvestOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency, uint128 amount)
    {
        return parseIncreaseInvestOrder(_msg);
    }

    /*
     * IncreaseRedeemOrder Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     * 57-72: currency (uint128 = 16 bytes)
     * 73-89: amount (uint128 = 16 bytes)
     */
    function formatIncreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.IncreaseRedeemOrder), poolId, trancheId, investor, currency, amount);
    }

    function isIncreaseRedeemOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.IncreaseRedeemOrder;
    }

    function parseIncreaseRedeemOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency, uint128 amount)
    {
        return parseIncreaseInvestOrder(_msg);
    }

    /*
     * DecreaseRedeemOrder Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     * 57-72: currency (uint128 = 16 bytes)
     * 73-89: amount (uint128 = 16 bytes)
     */
    function formatDecreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 amount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.DecreaseRedeemOrder), poolId, trancheId, investor, currency, amount);
    }

    function isDecreaseRedeemOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.DecreaseRedeemOrder;
    }

    function parseDecreaseRedeemOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency, uint128 amount)
    {
        return parseDecreaseInvestOrder(_msg);
    }

    /*
     * CollectInvest Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     */
    function formatCollectInvest(uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.CollectInvest), poolId, trancheId, investor, currency);
    }

    function isCollectInvest(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.CollectInvest;
    }

    function parseCollectInvest(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toBytes32(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
    }

    /*
     * CollectRedeem Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     */
    function formatCollectRedeem(uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.CollectRedeem), poolId, trancheId, investor, currency);
    }

    function isCollectRedeem(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.CollectRedeem;
    }

    function parseCollectRedeem(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toBytes32(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
    }

    function formatExecutedDecreaseInvestOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 remainingInvestOrder
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.ExecutedDecreaseInvestOrder),
            poolId,
            trancheId,
            investor,
            currency,
            currencyPayout,
            remainingInvestOrder
        );
    }

    function isExecutedDecreaseInvestOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.ExecutedDecreaseInvestOrder;
    }

    function parseExecutedDecreaseInvestOrder(bytes memory _msg)
        internal
        pure
        returns (
            uint64 poolId,
            bytes16 trancheId,
            address investor,
            uint128 currency,
            uint128 trancheTokenPayout,
            uint128 remainingInvestOrder
        )
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        trancheTokenPayout = BytesLib.toUint128(_msg, 73);
        remainingInvestOrder = BytesLib.toUint128(_msg, 89);
    }

    function formatExecutedDecreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 trancheTokenPayout,
        uint128 remainingRedeemOrder
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.ExecutedDecreaseRedeemOrder),
            poolId,
            trancheId,
            investor,
            currency,
            trancheTokenPayout,
            remainingRedeemOrder
        );
    }

    function isExecutedDecreaseRedeemOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.ExecutedDecreaseRedeemOrder;
    }

    function parseExecutedDecreaseRedeemOrder(bytes memory _msg)
        internal
        pure
        returns (
            uint64 poolId,
            bytes16 trancheId,
            address investor,
            uint128 currency,
            uint128 trancheTokensPayout,
            uint128 remainingRedeemOrder
        )
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        trancheTokensPayout = BytesLib.toUint128(_msg, 73);
        remainingRedeemOrder = BytesLib.toUint128(_msg, 89);
    }

    function formatExecutedCollectInvest(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 trancheTokensPayout,
        uint128 remainingInvestOrder
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.ExecutedCollectInvest),
            poolId,
            trancheId,
            investor,
            currency,
            currencyPayout,
            trancheTokensPayout,
            remainingInvestOrder
        );
    }

    function isExecutedCollectInvest(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.ExecutedCollectInvest;
    }

    function parseExecutedCollectInvest(bytes memory _msg)
        internal
        pure
        returns (
            uint64 poolId,
            bytes16 trancheId,
            address investor,
            uint128 currency,
            uint128 currencyPayout,
            uint128 trancheTokensPayout,
            uint128 remainingInvestOrder
        )
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        currencyPayout = BytesLib.toUint128(_msg, 73);
        trancheTokensPayout = BytesLib.toUint128(_msg, 89);
        remainingInvestOrder = BytesLib.toUint128(_msg, 105);
    }

    function formatExecutedCollectRedeem(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 currencyPayout,
        uint128 trancheTokensPayout,
        uint128 remainingRedeemOrder
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.ExecutedCollectRedeem),
            poolId,
            trancheId,
            investor,
            currency,
            currencyPayout,
            trancheTokensPayout,
            remainingRedeemOrder
        );
    }

    function isExecutedCollectRedeem(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.ExecutedCollectRedeem;
    }

    function parseExecutedCollectRedeem(bytes memory _msg)
        internal
        pure
        returns (
            uint64 poolId,
            bytes16 trancheId,
            address investor,
            uint128 currency,
            uint128 currencyPayout,
            uint128 trancheTokensPayout,
            uint128 remainingRedeemOrder
        )
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        currencyPayout = BytesLib.toUint128(_msg, 73);
        trancheTokensPayout = BytesLib.toUint128(_msg, 89);
        remainingRedeemOrder = BytesLib.toUint128(_msg, 105);
    }

    function formatScheduleUpgrade(address _contract) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.ScheduleUpgrade), _contract);
    }

    function isScheduleUpgrade(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.ScheduleUpgrade;
    }

    function parseScheduleUpgrade(bytes memory _msg) internal pure returns (address _contract) {
        _contract = BytesLib.toAddress(_msg, 1);
    }

    function formatCancelUpgrade(address _contract) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.CancelUpgrade), _contract);
    }

    function isCancelUpgrade(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.CancelUpgrade;
    }

    function parseCancelUpgrade(bytes memory _msg) internal pure returns (address _contract) {
        _contract = BytesLib.toAddress(_msg, 1);
    }

    /**
     * Update tranche token metadata
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-152: tokenName (string = 128 bytes)
     * 153-184: tokenSymbol (string = 32 bytes)
     */
    function formatUpdateTrancheTokenMetadata(
        uint64 poolId,
        bytes16 trancheId,
        string memory tokenName,
        string memory tokenSymbol
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.UpdateTrancheTokenMetadata),
            poolId,
            trancheId,
            _stringToBytes128(tokenName),
            _stringToBytes32(tokenSymbol)
        );
    }

    function isUpdateTrancheTokenMetadata(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.UpdateTrancheTokenMetadata;
    }

    function parseUpdateTrancheTokenMetadata(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        tokenName = _bytes128ToString(BytesLib.slice(_msg, 25, 128));
        tokenSymbol = _bytes32ToString(BytesLib.toBytes32(_msg, 153));
    }

    function formatCancelInvestOrder(uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.CancelInvestOrder), poolId, trancheId, investor, currency);
    }

    function parseCancelInvestOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
    }

    function formatCancelRedeemOrder(uint64 poolId, bytes16 trancheId, bytes32 investor, uint128 currency)
        internal
        pure
        returns (bytes memory)
    {
        return abi.encodePacked(uint8(Call.CancelRedeemOrder), poolId, trancheId, investor, currency);
    }

    function parseCancelRedeemOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, address investor, uint128 currency)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
    }

    /**
     * Disallow Investment Currency
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: currency (uint128 = 16 bytes)
     */
    function formatDisallowInvestmentCurrency(uint64 poolId, uint128 currency) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.DisallowInvestmentCurrency), poolId, currency);
    }

    function isDisallowInvestmentCurrency(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.DisallowInvestmentCurrency;
    }

    function parseDisallowInvestmentCurrency(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, uint128 currency)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        currency = BytesLib.toUint128(_msg, 9);
    }

    /**
     * Freeze
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-45: user (Ethereum address, 20 bytes - Skip 12 bytes from 32-byte addresses)
     *
     */
    function formatFreeze(uint64 poolId, bytes16 trancheId, address member) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.Freeze), poolId, trancheId, bytes32(bytes20(member)));
    }

    function isFreeze(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.Freeze;
    }

    function parseFreeze(bytes memory _msg) internal pure returns (uint64 poolId, bytes16 trancheId, address user) {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        user = BytesLib.toAddress(_msg, 25);
    }

    /**
     * Unfreeze
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-45: user (Ethereum address, 20 bytes - Skip 12 bytes from 32-byte addresses)
     *
     */
    function formatUnfreeze(uint64 poolId, bytes16 trancheId, address user) internal pure returns (bytes memory) {
        return abi.encodePacked(uint8(Call.Unfreeze), poolId, trancheId, bytes32(bytes20(user)));
    }

    function isUnfreeze(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.Unfreeze;
    }

    function parseUnfreeze(bytes memory _msg) internal pure returns (uint64 poolId, bytes16 trancheId, address user) {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        user = BytesLib.toAddress(_msg, 25);
    }

    /*
     * TriggerIncreaseRedeemOrder Message
     *
     * 0: call type (uint8 = 1 byte)
     * 1-8: poolId (uint64 = 8 bytes)
     * 9-24: trancheId (16 bytes)
     * 25-56: investor address (32 bytes)
     * 57-72: currency (uint128 = 16 bytes)
     * 73-89: amount (uint128 = 16 bytes)
     */
    function formatTriggerIncreaseRedeemOrder(
        uint64 poolId,
        bytes16 trancheId,
        bytes32 investor,
        uint128 currency,
        uint128 trancheTokenAmount
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(
            uint8(Call.TriggerIncreaseRedeemOrder), poolId, trancheId, investor, currency, trancheTokenAmount
        );
    }

    function isTriggerIncreaseRedeemOrder(bytes memory _msg) internal pure returns (bool) {
        return messageType(_msg) == Call.TriggerIncreaseRedeemOrder;
    }

    function parseTriggerIncreaseRedeemOrder(bytes memory _msg)
        internal
        pure
        returns (uint64 poolId, bytes16 trancheId, address investor, uint128 currency, uint128 trancheTokenAmount)
    {
        poolId = BytesLib.toUint64(_msg, 1);
        trancheId = BytesLib.toBytes16(_msg, 9);
        investor = BytesLib.toAddress(_msg, 25);
        currency = BytesLib.toUint128(_msg, 57);
        trancheTokenAmount = BytesLib.toUint128(_msg, 73);
    }

    // Utils
    function formatDomain(Domain domain) public pure returns (bytes9) {
        return bytes9(bytes1(uint8(domain)));
    }

    function formatDomain(Domain domain, uint64 chainId) public pure returns (bytes9) {
        return bytes9(BytesLib.slice(abi.encodePacked(uint8(domain), chainId), 0, 9));
    }

    function _stringToBytes128(string memory source) internal pure returns (bytes memory) {
        bytes memory temp = bytes(source);
        bytes memory result = new bytes(128);

        for (uint256 i = 0; i < 128; i++) {
            if (i < temp.length) {
                result[i] = temp[i];
            } else {
                result[i] = 0x00;
            }
        }

        return result;
    }

    function _bytes128ToString(bytes memory _bytes128) internal pure returns (string memory) {
        require(_bytes128.length == 128, "Input should be 128 bytes");

        uint8 i = 0;
        while (i < 128 && _bytes128[i] != 0) {
            i++;
        }

        bytes memory bytesArray = new bytes(i);

        for (uint8 j = 0; j < i; j++) {
            bytesArray[j] = _bytes128[j];
        }

        return string(bytesArray);
    }

    function _stringToBytes32(string memory source) internal pure returns (bytes32 result) {
        bytes memory tempEmptyStringTest = bytes(source);
        if (tempEmptyStringTest.length == 0) {
            return 0x0;
        }

        assembly {
            result := mload(add(source, 32))
        }
    }

    function _bytes32ToString(bytes32 _bytes32) internal pure returns (string memory) {
        uint8 i = 0;
        while (i < 32 && _bytes32[i] != 0) {
            i++;
        }
        bytes memory bytesArray = new bytes(i);
        for (i = 0; i < 32 && _bytes32[i] != 0; i++) {
            bytesArray[i] = _bytes32[i];
        }
        return string(bytesArray);
    }
}

// File: src/util/Auth.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.21;

/// @title  Auth
/// @notice Simple authentication pattern
/// @author Based on code from https://github.com/makerdao/dss
contract Auth {
    mapping(address => uint256) public wards;

    event Rely(address indexed user);
    event Deny(address indexed user);

    /// @dev Give permissions to the user
    function rely(address user) external auth {
        wards[user] = 1;
        emit Rely(user);
    }

    /// @dev Remove permissions from the user
    function deny(address user) external auth {
        wards[user] = 0;
        emit Deny(user);
    }

    /// @dev Check if the msg.sender has permissions
    modifier auth() {
        require(wards[msg.sender] == 1, "Auth/not-authorized");
        _;
    }
}

// File: src/util/BytesLib.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.21;

/// @title  Bytes Lib
/// @dev    Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
///         The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
/// @author Modified from Solidity Bytes Arrays Utils v0.8.0
library BytesLib {
    function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) {
        require(_length + 31 >= _length, "slice_overflow");
        require(_bytes.length >= _start + _length, "slice_outOfBounds");

        bytes memory tempBytes;

        assembly {
            switch iszero(_length)
            case 0 {
                // Get a location of some free memory and store it in tempBytes as
                // Solidity does for memory variables.
                tempBytes := mload(0x40)

                // The first word of the slice result is potentially a partial
                // word read from the original array. To read it, we calculate
                // the length of that partial word and start copying that many
                // bytes into the array. The first word we copy will start with
                // data we don't care about, but the last `lengthmod` bytes will
                // land at the beginning of the contents of the new array. When
                // we're done copying, we overwrite the full first word with
                // the actual length of the slice.
                let lengthmod := and(_length, 31)

                // The multiplication in the next line is necessary
                // because when slicing multiples of 32 bytes (lengthmod == 0)
                // the following copy loop was copying the origin's length
                // and then ending prematurely not copying everything it should.
                let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
                let end := add(mc, _length)

                for {
                    // The multiplication in the next line has the same exact purpose
                    // as the one above.
                    let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
                } lt(mc, end) {
                    mc := add(mc, 0x20)
                    cc := add(cc, 0x20)
                } { mstore(mc, mload(cc)) }

                mstore(tempBytes, _length)

                //update free-memory pointer
                //allocating the array padded to 32 bytes like the compiler does now
                mstore(0x40, and(add(mc, 31), not(31)))
            }
            //if we want a zero-length slice let's just return a zero-length array
            default {
                tempBytes := mload(0x40)
                //zero out the 32 bytes slice we are about to return
                //we need to do it because Solidity does not garbage collect
                mstore(tempBytes, 0)

                mstore(0x40, add(tempBytes, 0x20))
            }
        }

        return tempBytes;
    }

    function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
        require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
        address tempAddress;

        assembly {
            tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
        }

        return tempAddress;
    }

    function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
        require(_bytes.length >= _start + 1, "toUint8_outOfBounds");
        uint8 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x1), _start))
        }

        return tempUint;
    }

    function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) {
        require(_bytes.length >= _start + 8, "toUint64_outOfBounds");
        uint64 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x8), _start))
        }

        return tempUint;
    }

    function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) {
        require(_bytes.length >= _start + 16, "toUint128_outOfBounds");
        uint128 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x10), _start))
        }

        return tempUint;
    }

    function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) {
        require(_bytes.length >= _start + 32, "toBytes32_outOfBounds");
        bytes32 tempBytes32;

        assembly {
            tempBytes32 := mload(add(add(_bytes, 0x20), _start))
        }

        return tempBytes32;
    }

    function toBytes16(bytes memory _bytes, uint256 _start) internal pure returns (bytes16) {
        require(_bytes.length >= _start + 16, "toBytes16_outOfBounds");
        bytes16 tempBytes16;

        assembly {
            tempBytes16 := mload(add(add(_bytes, 0x20), _start))
        }

        return tempBytes16;
    }
}