ETH Price: $2,434.60 (+1.13%)

Transaction Decoder

Block:
19908341 at May-20-2024 03:05:23 AM +UTC
Transaction Fee:
0.000234331497794988 ETH $0.57
Gas Used:
80,674 Gas / 2.904671862 Gwei

Emitted Events:

290 FastBridge.BridgeRequested( transactionId=DD80FF3C76D56CCF12A9021A21D180FE342127E969E146B45FBB5CCAE450EE8C, sender=[Sender] 0x0c181ab71554bc126c5bf4ef6ca68bb495287873, request=0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000A4B10000000000000000000000000C181AB71554BC126C5BF4EF6CA68BB4952878730000000000000000000000000C181AB71554BC126C5BF4EF6CA68BB495287873000000000000000000000000EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE000000000000000000000000EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE0000000000000000000000000000000000000000000000000DE000CD866F80000000000000000000000000000000000000000000000000000DDDAB1DCDB360CA0000000000000000000000000000000000000000000000000000B5E620F48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000664AF6270000000000000000000000000000000000000000000000000000000000001177, destChainId=42161, originToken=0xEeeeeEee...eeeeeEEeE, destToken=0xEeeeeEee...eeeeeEEeE, originAmount=999800000000000000, destAmount=999142836839604426, sendChainGas=False )

Account State Difference:

  Address   Before After State Difference Code
0x0c181aB7...495287873
1.067484876349007178 Eth
Nonce: 1201
0.06725054485121219 Eth
Nonce: 1202
1.000234331497794988
0x5523D3c9...B1B0fB59E
(Synapse : Fast Bridge RFQ)
3.935937320636751917 Eth4.935937320636751917 Eth1
(beaverbuild)
16.297375446132723733 Eth16.297377882499624833 Eth0.0000024363669011

Execution Trace

ETH 1 FastBridgeRouter.bridge( recipient=0x0c181aB71554bc126c5bF4EF6ca68bB495287873, chainId=42161, token=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount=1000000000000000000, originQuery=[{name:routerAdapter, type:address, order:1, indexed:false, value:0x0000000000000000000000000000000000000000, valueString:0x0000000000000000000000000000000000000000}, {name:tokenOut, type:address, order:2, indexed:false, value:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, valueString:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE}, {name:minAmountOut, type:uint256, order:3, indexed:false, value:999957141841980222, valueString:999957141841980222}, {name:deadline, type:uint256, order:4, indexed:false, value:1716174911, valueString:1716174911}, {name:rawParams, type:bytes, order:5, indexed:false, value:0x, valueString:0x}], destQuery=[{name:routerAdapter, type:address, order:1, indexed:false, value:0x0000000000000000000000000000000000000000, valueString:0x0000000000000000000000000000000000000000}, {name:tokenOut, type:address, order:2, indexed:false, value:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, valueString:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE}, {name:minAmountOut, type:uint256, order:3, indexed:false, value:999142836839604426, valueString:999142836839604426}, {name:deadline, type:uint256, order:4, indexed:false, value:1716188711, valueString:1716188711}, {name:rawParams, type:bytes, order:5, indexed:false, value:0x, valueString:0x}] )
  • ETH 1 FastBridge.bridge( params=[{name:dstChainId, type:uint32, order:1, indexed:false, value:42161, valueString:42161}, {name:sender, type:address, order:2, indexed:false, value:0x0c181aB71554bc126c5bF4EF6ca68bB495287873, valueString:0x0c181aB71554bc126c5bF4EF6ca68bB495287873}, {name:to, type:address, order:3, indexed:false, value:0x0c181aB71554bc126c5bF4EF6ca68bB495287873, valueString:0x0c181aB71554bc126c5bF4EF6ca68bB495287873}, {name:originToken, type:address, order:4, indexed:false, value:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, valueString:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE}, {name:destToken, type:address, order:5, indexed:false, value:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, valueString:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE}, {name:originAmount, type:uint256, order:6, indexed:false, value:1000000000000000000, valueString:1000000000000000000}, {name:destAmount, type:uint256, order:7, indexed:false, value:999142836839604426, valueString:999142836839604426}, {name:sendChainGas, type:bool, order:8, indexed:false, value:false, valueString:False}, {name:deadline, type:uint256, order:9, indexed:false, value:1716188711, valueString:1716188711}] )
    File 1 of 2: FastBridgeRouter
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    import {DefaultRouter} from "../router/DefaultRouter.sol";
    import {UniversalTokenLib} from "../router/libs/UniversalToken.sol";
    import {ActionLib, LimitedToken} from "../router/libs/Structs.sol";
    import {IFastBridge} from "./interfaces/IFastBridge.sol";
    import {IFastBridgeRouter, SwapQuery} from "./interfaces/IFastBridgeRouter.sol";
    import {ISwapQuoter} from "./interfaces/ISwapQuoter.sol";
    import {Ownable} from "@openzeppelin/contracts-4.5.0/access/Ownable.sol";
    contract FastBridgeRouter is DefaultRouter, Ownable, IFastBridgeRouter {
        using UniversalTokenLib for address;
        /// @notice Emitted when the swap quoter is set.
        /// @param newSwapQuoter The new swap quoter.
        event SwapQuoterSet(address newSwapQuoter);
        /// @notice Emitted when the new FastBridge contract is set.
        /// @param newFastBridge The new FastBridge contract.
        event FastBridgeSet(address newFastBridge);
        /// @inheritdoc IFastBridgeRouter
        bytes1 public constant GAS_REBATE_FLAG = 0x2A;
        /// @inheritdoc IFastBridgeRouter
        address public fastBridge;
        /// @inheritdoc IFastBridgeRouter
        address public swapQuoter;
        constructor(address owner_) {
            transferOwnership(owner_);
        }
        /// @inheritdoc IFastBridgeRouter
        function setFastBridge(address fastBridge_) external onlyOwner {
            fastBridge = fastBridge_;
            emit FastBridgeSet(fastBridge_);
        }
        /// @inheritdoc IFastBridgeRouter
        function setSwapQuoter(address swapQuoter_) external onlyOwner {
            swapQuoter = swapQuoter_;
            emit SwapQuoterSet(swapQuoter_);
        }
        /// @inheritdoc IFastBridgeRouter
        function bridge(
            address recipient,
            uint256 chainId,
            address token,
            uint256 amount,
            SwapQuery memory originQuery,
            SwapQuery memory destQuery
        ) external payable {
            if (originQuery.hasAdapter()) {
                // Perform a swap using the swap adapter, set this contract as recipient
                (token, amount) = _doSwap(address(this), token, amount, originQuery);
            } else {
                // Otherwise, pull the token from the user to this contract
                amount = _pullToken(address(this), token, amount);
            }
            IFastBridge.BridgeParams memory params = IFastBridge.BridgeParams({
                dstChainId: uint32(chainId),
                sender: msg.sender,
                to: recipient,
                originToken: token,
                destToken: destQuery.tokenOut,
                originAmount: amount,
                destAmount: destQuery.minAmountOut,
                sendChainGas: _chainGasRequested(destQuery.rawParams),
                deadline: destQuery.deadline
            });
            token.universalApproveInfinity(fastBridge, amount);
            uint256 msgValue = token == UniversalTokenLib.ETH_ADDRESS ? amount : 0;
            IFastBridge(fastBridge).bridge{value: msgValue}(params);
        }
        /// @inheritdoc IFastBridgeRouter
        function getOriginAmountOut(
            address tokenIn,
            address[] memory rfqTokens,
            uint256 amountIn
        ) external view returns (SwapQuery[] memory originQueries) {
            uint256 len = rfqTokens.length;
            originQueries = new SwapQuery[](len);
            for (uint256 i = 0; i < len; ++i) {
                originQueries[i] = ISwapQuoter(swapQuoter).getAmountOut(
                    LimitedToken({actionMask: ActionLib.allActions(), token: tokenIn}),
                    rfqTokens[i],
                    amountIn
                );
                // Adjust the Adapter address if it exists
                if (originQueries[i].hasAdapter()) {
                    originQueries[i].routerAdapter = address(this);
                }
            }
        }
        /// @dev Checks if the explicit instruction to send gas to the destination chain was provided.
        function _chainGasRequested(bytes memory rawParams) internal pure returns (bool) {
            return rawParams.length > 0 && rawParams[0] == GAS_REBATE_FLAG;
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    import {DefaultAdapter} from "./adapters/DefaultAdapter.sol";
    import {IRouterAdapter} from "./interfaces/IRouterAdapter.sol";
    import {DeadlineExceeded, InsufficientOutputAmount, MsgValueIncorrect, TokenNotETH} from "./libs/Errors.sol";
    import {Action, DefaultParams, SwapQuery} from "./libs/Structs.sol";
    import {UniversalTokenLib} from "./libs/UniversalToken.sol";
    import {SafeERC20, IERC20} from "@openzeppelin/contracts-4.5.0/token/ERC20/utils/SafeERC20.sol";
    /// @title DefaultRouter
    /// @notice Base contract for all Synapse Routers, that is able to natively work with Default Pools
    /// due to the fact that it inherits from DefaultAdapter.
    abstract contract DefaultRouter is DefaultAdapter {
        using SafeERC20 for IERC20;
        using UniversalTokenLib for address;
        /// @dev Performs a "swap from tokenIn" following instructions from `query`.
        /// `query` will include the router adapter to use, and the exact type of "tokenIn -> tokenOut swap"
        /// should be encoded in `query.rawParams`.
        function _doSwap(
            address recipient,
            address tokenIn,
            uint256 amountIn,
            SwapQuery memory query
        ) internal returns (address tokenOut, uint256 amountOut) {
            // First, check the deadline for the swap
            // solhint-disable-next-line not-rely-on-time
            if (block.timestamp > query.deadline) revert DeadlineExceeded();
            // Pull initial token from the user to specified router adapter
            amountIn = _pullToken(query.routerAdapter, tokenIn, amountIn);
            tokenOut = query.tokenOut;
            address routerAdapter = query.routerAdapter;
            if (routerAdapter == address(this)) {
                // If the router adapter is this contract, we can perform the swap directly and trust the result
                amountOut = _adapterSwap(recipient, tokenIn, amountIn, tokenOut, query.rawParams);
            } else {
                // Otherwise, we need to call the router adapter. Adapters are permissionless, so we verify the result
                // Record tokenOut balance before swap
                amountOut = tokenOut.universalBalanceOf(recipient);
                IRouterAdapter(routerAdapter).adapterSwap{value: msg.value}({
                    recipient: recipient,
                    tokenIn: tokenIn,
                    amountIn: amountIn,
                    tokenOut: tokenOut,
                    rawParams: query.rawParams
                });
                // Use the difference between the recorded balance and the current balance as the amountOut
                amountOut = tokenOut.universalBalanceOf(recipient) - amountOut;
            }
            // Finally, check that the recipient received at least as much as they wanted
            if (amountOut < query.minAmountOut) revert InsufficientOutputAmount();
        }
        /// @dev Pulls a requested token from the user to the requested recipient.
        /// Or, if msg.value was provided, check that ETH_ADDRESS was used and msg.value is correct.
        function _pullToken(
            address recipient,
            address token,
            uint256 amount
        ) internal returns (uint256 amountPulled) {
            if (msg.value == 0) {
                token.assertIsContract();
                // Record token balance before transfer
                amountPulled = IERC20(token).balanceOf(recipient);
                // Token needs to be pulled only if msg.value is zero
                // This way user can specify WETH as the origin asset
                IERC20(token).safeTransferFrom(msg.sender, recipient, amount);
                // Use the difference between the recorded balance and the current balance as the amountPulled
                amountPulled = IERC20(token).balanceOf(recipient) - amountPulled;
            } else {
                // Otherwise, we need to check that ETH was specified
                if (token != UniversalTokenLib.ETH_ADDRESS) revert TokenNotETH();
                // And that amount matches msg.value
                if (amount != msg.value) revert MsgValueIncorrect();
                // We will forward msg.value in the external call later, if recipient is not this contract
                amountPulled = msg.value;
            }
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    import {TokenNotContract} from "./Errors.sol";
    import {SafeERC20, IERC20} from "@openzeppelin/contracts-4.5.0/token/ERC20/utils/SafeERC20.sol";
    library UniversalTokenLib {
        using SafeERC20 for IERC20;
        address internal constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
        /// @notice Transfers tokens to the given account. Reverts if transfer is not successful.
        /// @dev This might trigger fallback, if ETH is transferred to the contract.
        /// Make sure this can not lead to reentrancy attacks.
        function universalTransfer(
            address token,
            address to,
            uint256 value
        ) internal {
            // Don't do anything, if need to send tokens to this address
            if (to == address(this)) return;
            if (token == ETH_ADDRESS) {
                /// @dev Note: this can potentially lead to executing code in `to`.
                // solhint-disable-next-line avoid-low-level-calls
                (bool success, ) = to.call{value: value}("");
                require(success, "ETH transfer failed");
            } else {
                IERC20(token).safeTransfer(to, value);
            }
        }
        /// @notice Issues an infinite allowance to the spender, if the current allowance is insufficient
        /// to spend the given amount.
        function universalApproveInfinity(
            address token,
            address spender,
            uint256 amountToSpend
        ) internal {
            // ETH Chad doesn't require your approval
            if (token == ETH_ADDRESS) return;
            // No-op if allowance is already sufficient
            uint256 allowance = IERC20(token).allowance(address(this), spender);
            if (allowance >= amountToSpend) return;
            // Otherwise, reset approval to 0 and set to max allowance
            if (allowance > 0) IERC20(token).safeApprove(spender, 0);
            IERC20(token).safeApprove(spender, type(uint256).max);
        }
        /// @notice Returns the balance of the given token (or native ETH) for the given account.
        function universalBalanceOf(address token, address account) internal view returns (uint256) {
            if (token == ETH_ADDRESS) {
                return account.balance;
            } else {
                return IERC20(token).balanceOf(account);
            }
        }
        /// @dev Checks that token is a contract and not ETH_ADDRESS.
        function assertIsContract(address token) internal view {
            // Check that ETH_ADDRESS was not used (in case this is a predeploy on any of the chains)
            if (token == UniversalTokenLib.ETH_ADDRESS) revert TokenNotContract();
            // Check that token is not an EOA
            if (token.code.length == 0) revert TokenNotContract();
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity >=0.8.13; // "using A for B global" requires 0.8.13 or higher
    // ══════════════════════════════════════════ TOKEN AND POOL DESCRIPTION ═══════════════════════════════════════════════
    /// @notice Struct representing a bridge token. Used as the return value in view functions.
    /// @param symbol   Bridge token symbol: unique token ID consistent among all chains
    /// @param token    Bridge token address
    struct BridgeToken {
        string symbol;
        address token;
    }
    /// @notice Struct used by IPoolHandler to represent a token in a pool
    /// @param index    Token index in the pool
    /// @param token    Token address
    struct IndexedToken {
        uint8 index;
        address token;
    }
    /// @notice Struct representing a token, and the available Actions for performing a swap.
    /// @param actionMask   Bitmask representing what actions (see ActionLib) are available for swapping a token
    /// @param token        Token address
    struct LimitedToken {
        uint256 actionMask;
        address token;
    }
    /// @notice Struct representing how pool tokens are stored by `SwapQuoter`.
    /// @param isWeth   Whether the token represents Wrapped ETH.
    /// @param token    Token address.
    struct PoolToken {
        bool isWeth;
        address token;
    }
    /// @notice Struct representing a liquidity pool. Used as the return value in view functions.
    /// @param pool         Pool address.
    /// @param lpToken      Address of pool's LP token.
    /// @param tokens       List of pool's tokens.
    struct Pool {
        address pool;
        address lpToken;
        PoolToken[] tokens;
    }
    // ════════════════════════════════════════════════ ROUTER STRUCTS ═════════════════════════════════════════════════════
    /// @notice Struct representing a quote request for swapping a bridge token.
    /// Used in destination chain's SynapseRouter, hence the name "Destination Request".
    /// @dev tokenOut is passed externally.
    /// @param symbol   Bridge token symbol: unique token ID consistent among all chains
    /// @param amountIn Amount of bridge token to start with, before the bridge fee is applied
    struct DestRequest {
        string symbol;
        uint256 amountIn;
    }
    /// @notice Struct representing a swap request for SynapseRouter.
    /// @dev tokenIn is supplied separately.
    /// @param routerAdapter    Contract that will perform the swap for the Router. Address(0) specifies a "no swap" query.
    /// @param tokenOut         Token address to swap to.
    /// @param minAmountOut     Minimum amount of tokens to receive after the swap, or tx will be reverted.
    /// @param deadline         Latest timestamp for when the transaction needs to be executed, or tx will be reverted.
    /// @param rawParams        ABI-encoded params for the swap that will be passed to `routerAdapter`.
    ///                         Should be DefaultParams for swaps via DefaultAdapter.
    struct SwapQuery {
        address routerAdapter;
        address tokenOut;
        uint256 minAmountOut;
        uint256 deadline;
        bytes rawParams;
    }
    using SwapQueryLib for SwapQuery global;
    library SwapQueryLib {
        /// @notice Checks whether the router adapter was specified in the query.
        /// Query without a router adapter specifies that no action needs to be taken.
        function hasAdapter(SwapQuery memory query) internal pure returns (bool) {
            return query.routerAdapter != address(0);
        }
        /// @notice Fills `routerAdapter` and `deadline` fields in query, if it specifies one of the supported Actions,
        /// and if a path for this action was found.
        function fillAdapterAndDeadline(SwapQuery memory query, address routerAdapter) internal pure {
            // Fill the fields only if some path was found.
            if (query.minAmountOut == 0) return;
            // Empty params indicates no action needs to be done, thus no adapter is needed.
            query.routerAdapter = query.rawParams.length == 0 ? address(0) : routerAdapter;
            // Set default deadline to infinity. Not using the value of 0,
            // which would lead to every swap to revert by default.
            query.deadline = type(uint256).max;
        }
    }
    // ════════════════════════════════════════════════ ADAPTER STRUCTS ════════════════════════════════════════════════════
    /// @notice Struct representing parameters for swapping via DefaultAdapter.
    /// @param action           Action that DefaultAdapter needs to perform.
    /// @param pool             Liquidity pool that will be used for Swap/AddLiquidity/RemoveLiquidity actions.
    /// @param tokenIndexFrom   Token index to swap from. Used for swap/addLiquidity actions.
    /// @param tokenIndexTo     Token index to swap to. Used for swap/removeLiquidity actions.
    struct DefaultParams {
        Action action;
        address pool;
        uint8 tokenIndexFrom;
        uint8 tokenIndexTo;
    }
    /// @notice All possible actions that DefaultAdapter could perform.
    enum Action {
        Swap, // swap between two pools tokens
        AddLiquidity, // add liquidity in a form of a single pool token
        RemoveLiquidity, // remove liquidity in a form of a single pool token
        HandleEth // ETH <> WETH interaction
    }
    using ActionLib for Action global;
    /// @notice Library for dealing with bit masks which describe what set of Actions is available.
    library ActionLib {
        /// @notice Returns a bitmask with all possible actions set to True.
        function allActions() internal pure returns (uint256 actionMask) {
            actionMask = type(uint256).max;
        }
        /// @notice Returns whether the given action is set to True in the bitmask.
        function isIncluded(Action action, uint256 actionMask) internal pure returns (bool) {
            return actionMask & mask(action) != 0;
        }
        /// @notice Returns a bitmask with only the given action set to True.
        function mask(Action action) internal pure returns (uint256) {
            return 1 << uint256(action);
        }
        /// @notice Returns a bitmask with only two given actions set to True.
        function mask(Action a, Action b) internal pure returns (uint256) {
            return mask(a) | mask(b);
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    // TODO: This should be pulled from the sanguine repo (requires publish and relaxing the ^0.8.20 pragma)
    interface IFastBridge {
        struct BridgeTransaction {
            uint32 originChainId;
            uint32 destChainId;
            address originSender; // user (origin)
            address destRecipient; // user (dest)
            address originToken;
            address destToken;
            uint256 originAmount; // amount in on origin bridge less originFeeAmount
            uint256 destAmount;
            uint256 originFeeAmount;
            bool sendChainGas;
            uint256 deadline;
            uint256 nonce;
        }
        struct BridgeProof {
            uint96 timestamp;
            address relayer;
        }
        // ============ Events ============
        event BridgeRequested(bytes32 transactionId, address sender, bytes request);
        event BridgeRelayed(
            bytes32 transactionId,
            address relayer,
            address to,
            address token,
            uint256 amount,
            uint256 chainGasAmount
        );
        event BridgeProofProvided(bytes32 transactionId, address relayer, bytes32 transactionHash);
        event BridgeProofDisputed(bytes32 transactionId, address relayer);
        event BridgeDepositClaimed(bytes32 transactionId, address relayer, address to, address token, uint256 amount);
        event BridgeDepositRefunded(bytes32 transactionId, address to, address token, uint256 amount);
        // ============ Methods ============
        struct BridgeParams {
            uint32 dstChainId;
            address sender;
            address to;
            address originToken;
            address destToken;
            uint256 originAmount; // should include protocol fee (if any)
            uint256 destAmount; // should include relayer fee
            bool sendChainGas;
            uint256 deadline;
        }
        /// @notice Initiates bridge on origin chain to be relayed by off-chain relayer
        /// @param params The parameters required to bridge
        function bridge(BridgeParams memory params) external payable;
        /// @notice Relays destination side of bridge transaction by off-chain relayer
        /// @param request The encoded bridge transaction to relay on destination chain
        function relay(bytes memory request) external payable;
        /// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction
        /// @param request The encoded bridge transaction to prove on origin chain
        /// @param destTxHash The destination tx hash proving bridge transaction was relayed
        function prove(bytes memory request, bytes32 destTxHash) external;
        /// @notice Completes bridge transaction on origin chain by claiming originally deposited capital
        /// @param request The encoded bridge transaction to claim on origin chain
        /// @param to The recipient address of the funds
        function claim(bytes memory request, address to) external;
        /// @notice Disputes an outstanding proof in case relayer provided dest chain tx is invalid
        /// @param transactionId The transaction id associated with the encoded bridge transaction to dispute
        function dispute(bytes32 transactionId) external;
        /// @notice Refunds an outstanding bridge transaction in case optimistic bridging failed
        /// @param request The encoded bridge transaction to refund
        /// @param to The recipient address of the funds
        function refund(bytes memory request, address to) external;
        // ============ Views ============
        /// @notice Decodes bridge request into a bridge transaction
        /// @param request The bridge request to decode
        function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory);
        /// @notice Checks if the dispute period has passed so bridge deposit can be claimed
        /// @param transactionId The transaction id associated with the encoded bridge transaction to check
        /// @param relayer The address of the relayer attempting to claim
        function canClaim(bytes32 transactionId, address relayer) external view returns (bool);
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    import {SwapQuery} from "../../router/libs/Structs.sol";
    interface IFastBridgeRouter {
        /// @notice Sets the address of the FastBridge contract
        /// @dev This function is only callable by the owner
        /// @param fastBridge_      The address of the FastBridge contract
        function setFastBridge(address fastBridge_) external;
        /// @notice Sets the address of the SwapQuoter contract
        /// @dev This function is only callable by the owner
        /// @param swapQuoter_      The address of the SwapQuoter contract
        function setSwapQuoter(address swapQuoter_) external;
        /// @notice Initiate an RFQ transaction with an optional swap on origin chain,
        /// and an optional gas rebate on destination chain.
        /// @dev Note that method is payable.
        /// If token is ETH_ADDRESS, this method should be invoked with `msg.value = amountIn`.
        /// If token is ERC20, the tokens will be pulled from msg.sender (use `msg.value = 0`).
        /// Make sure to approve this contract for spending `token` beforehand.
        ///
        /// `originQuery` is supposed to be fetched using FastBridgeRouter.getOriginAmountOut().
        /// Alternatively one could use an external adapter for more complex swaps on the origin chain.
        /// `destQuery.rawParams` signals whether the user wants to receive a gas rebate on the destination chain:
        /// - If the first byte of `destQuery.rawParams` is GAS_REBATE_FLAG, the user wants to receive a gas rebate.
        /// - Otherwise, the user does not want to receive a gas rebate.
        ///
        /// Cross-chain RFQ swap will be performed between tokens: `originQuery.tokenOut` and `destQuery.tokenOut`.
        /// Note: both tokens could be ETH_ADDRESS or ERC20.
        /// Full proceeds of the origin swap are considered the bid for the cross-chain swap.
        /// `destQuery.minAmountOut` is considered the ask for the cross-chain swap.
        /// Note: applying slippage to `destQuery.minAmountOut` will result in a worse price for the user,
        /// the full Relayer quote should be used instead.
        /// @param recipient        Address to receive tokens on destination chain
        /// @param chainId          Destination chain id
        /// @param token            Initial token to be pulled from the user
        /// @param amount           Amount of the initial tokens to be pulled from the user
        /// @param originQuery      Origin swap query (see above)
        /// @param destQuery        Destination swap query (see above)
        function bridge(
            address recipient,
            uint256 chainId,
            address token,
            uint256 amount,
            SwapQuery memory originQuery,
            SwapQuery memory destQuery
        ) external payable;
        // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════
        /// @notice Finds the best path between `tokenIn` and every RFQ token from the given list,
        /// treating the swap as "origin swap", without putting any restrictions on the swap.
        /// @dev Check (query.minAmountOut != 0): this is true only if the swap is possible.
        /// The returned queries with minAmountOut != 0 could be used as `originQuery` with FastBridgeRouter.
        /// Note: it is possible to form a SwapQuery off-chain using alternative SwapAdapter for the origin swap.
        /// @param tokenIn          Initial token that user wants to bridge/swap
        /// @param rfqTokens        List of RFQ tokens
        /// @param amountIn         Amount of tokens user wants to bridge/swap
        /// @return originQueries   List of structs that could be used as `originQuery` in FastBridgeRouter.
        ///                         minAmountOut and deadline fields will need to be adjusted based on the user settings.
        function getOriginAmountOut(
            address tokenIn,
            address[] memory rfqTokens,
            uint256 amountIn
        ) external view returns (SwapQuery[] memory originQueries);
        /// @notice Magic value that indicates that the user wants to receive gas rebate on the destination chain.
        /// This is the answer to the ultimate question of life, the universe, and everything.
        function GAS_REBATE_FLAG() external view returns (bytes1);
        /// @notice Address of the FastBridge contract, used to initiate cross-chain RFQ swaps.
        function fastBridge() external view returns (address);
        /// @notice Address of the SwapQuoter contract, used to fetch quotes for the origin swap.
        function swapQuoter() external view returns (address);
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    import {LimitedToken, SwapQuery} from "../../router/libs/Structs.sol";
    interface ISwapQuoter {
        function getAmountOut(
            LimitedToken memory tokenIn,
            address tokenOut,
            uint256 amountIn
        ) external view returns (SwapQuery memory query);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)
    pragma solidity ^0.8.0;
    import "../utils/Context.sol";
    /**
     * @dev Contract module which provides a basic access control mechanism, where
     * there is an account (an owner) that can be granted exclusive access to
     * specific functions.
     *
     * By default, the owner account will be the one that deploys the contract. This
     * can later be changed with {transferOwnership}.
     *
     * This module is used through inheritance. It will make available the modifier
     * `onlyOwner`, which can be applied to your functions to restrict their use to
     * the owner.
     */
    abstract contract Ownable is Context {
        address private _owner;
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
        /**
         * @dev Initializes the contract setting the deployer as the initial owner.
         */
        constructor() {
            _transferOwnership(_msgSender());
        }
        /**
         * @dev Returns the address of the current owner.
         */
        function owner() public view virtual returns (address) {
            return _owner;
        }
        /**
         * @dev Throws if called by any account other than the owner.
         */
        modifier onlyOwner() {
            require(owner() == _msgSender(), "Ownable: caller is not the owner");
            _;
        }
        /**
         * @dev Leaves the contract without owner. It will not be possible to call
         * `onlyOwner` functions anymore. Can only be called by the current owner.
         *
         * NOTE: Renouncing ownership will leave the contract without an owner,
         * thereby removing any functionality that is only available to the owner.
         */
        function renounceOwnership() public virtual onlyOwner {
            _transferOwnership(address(0));
        }
        /**
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Can only be called by the current owner.
         */
        function transferOwnership(address newOwner) public virtual onlyOwner {
            require(newOwner != address(0), "Ownable: new owner is the zero address");
            _transferOwnership(newOwner);
        }
        /**
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Internal function without access restriction.
         */
        function _transferOwnership(address newOwner) internal virtual {
            address oldOwner = _owner;
            _owner = newOwner;
            emit OwnershipTransferred(oldOwner, newOwner);
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    import {IDefaultPool, IDefaultExtendedPool} from "../interfaces/IDefaultExtendedPool.sol";
    import {IRouterAdapter} from "../interfaces/IRouterAdapter.sol";
    import {IWETH9} from "../interfaces/IWETH9.sol";
    import {MsgValueIncorrect, PoolNotFound, TokenAddressMismatch, TokensIdentical} from "../libs/Errors.sol";
    import {Action, DefaultParams} from "../libs/Structs.sol";
    import {UniversalTokenLib} from "../libs/UniversalToken.sol";
    import {SafeERC20, IERC20} from "@openzeppelin/contracts-4.5.0/token/ERC20/utils/SafeERC20.sol";
    contract DefaultAdapter is IRouterAdapter {
        using SafeERC20 for IERC20;
        using UniversalTokenLib for address;
        /// @notice Enable this contract to receive Ether when withdrawing from WETH.
        /// @dev Consider implementing rescue functions to withdraw Ether from this contract.
        receive() external payable {}
        /// @inheritdoc IRouterAdapter
        function adapterSwap(
            address recipient,
            address tokenIn,
            uint256 amountIn,
            address tokenOut,
            bytes memory rawParams
        ) external payable returns (uint256 amountOut) {
            return _adapterSwap(recipient, tokenIn, amountIn, tokenOut, rawParams);
        }
        /// @dev Internal logic for doing a tokenIn -> tokenOut swap.
        /// Note: `tokenIn` is assumed to have already been transferred to this contract.
        function _adapterSwap(
            address recipient,
            address tokenIn,
            uint256 amountIn,
            address tokenOut,
            bytes memory rawParams
        ) internal virtual returns (uint256 amountOut) {
            // We define a few phases for the whole Adapter's swap process.
            // (?) means the phase is optional.
            // (!) means the phase is mandatory.
            // PHASE 0(!): CHECK ALL THE PARAMS
            DefaultParams memory params = _checkParams(tokenIn, tokenOut, rawParams);
            // PHASE 1(?): WRAP RECEIVED ETH INTO WETH
            tokenIn = _wrapReceivedETH(tokenIn, amountIn, tokenOut, params);
            // After PHASE 1 this contract has `amountIn` worth of `tokenIn`, tokenIn != ETH_ADDRESS
            // PHASE 2(?): PREPARE TO UNWRAP SWAPPED WETH
            address tokenSwapTo = _deriveTokenSwapTo(tokenIn, tokenOut, params);
            // We need to perform tokenIn -> tokenSwapTo action in PHASE 3.
            // if tokenOut == ETH_ADDRESS, we need to unwrap WETH in PHASE 4.
            // Recipient will receive `tokenOut` in PHASE 5.
            // PHASE 3(?): PERFORM A REQUESTED SWAP
            amountOut = _performPoolAction(tokenIn, amountIn, tokenSwapTo, params);
            // After PHASE 3 this contract has `amountOut` worth of `tokenSwapTo`, tokenSwapTo != ETH_ADDRESS
            // PHASE 4(?): UNWRAP SWAPPED WETH
            // Check if the final token is native ETH
            if (tokenOut == UniversalTokenLib.ETH_ADDRESS) {
                // PHASE 2: WETH address was stored as `tokenSwapTo`
                _unwrapETH(tokenSwapTo, amountOut);
            }
            // PHASE 5(!): TRANSFER SWAPPED TOKENS TO RECIPIENT
            // Note: this will transfer native ETH, if tokenOut == ETH_ADDRESS
            // Note: this is a no-op if recipient == address(this)
            tokenOut.universalTransfer(recipient, amountOut);
        }
        /// @dev Checks the params and decodes them into a struct.
        function _checkParams(
            address tokenIn,
            address tokenOut,
            bytes memory rawParams
        ) internal pure returns (DefaultParams memory params) {
            if (tokenIn == tokenOut) revert TokensIdentical();
            // Decode params for swapping via a Default pool
            params = abi.decode(rawParams, (DefaultParams));
            // Swap pool should exist, if action other than HandleEth was requested
            if (params.pool == address(0) && params.action != Action.HandleEth) revert PoolNotFound();
        }
        /// @dev Wraps native ETH into WETH, if requested.
        /// Returns the address of the token this contract ends up with.
        function _wrapReceivedETH(
            address tokenIn,
            uint256 amountIn,
            address tokenOut,
            DefaultParams memory params
        ) internal returns (address wrappedTokenIn) {
            // tokenIn was already transferred to this contract, check if we start from native ETH
            if (tokenIn == UniversalTokenLib.ETH_ADDRESS) {
                // Determine WETH address: this is either tokenOut (if no swap is needed),
                // or a pool token with index `tokenIndexFrom` (if swap is needed).
                wrappedTokenIn = _deriveWethAddress({token: tokenOut, params: params, isTokenFromWeth: true});
                // Wrap ETH into WETH and leave it in this contract
                _wrapETH(wrappedTokenIn, amountIn);
            } else {
                wrappedTokenIn = tokenIn;
                // For ERC20 tokens msg.value should be zero
                if (msg.value != 0) revert MsgValueIncorrect();
            }
        }
        /// @dev Derives the address of token to be received after an action defined in `params`.
        function _deriveTokenSwapTo(
            address tokenIn,
            address tokenOut,
            DefaultParams memory params
        ) internal view returns (address tokenSwapTo) {
            // Check if swap to native ETH was requested
            if (tokenOut == UniversalTokenLib.ETH_ADDRESS) {
                // Determine WETH address: this is either tokenIn (if no swap is needed),
                // or a pool token with index `tokenIndexTo` (if swap is needed).
                tokenSwapTo = _deriveWethAddress({token: tokenIn, params: params, isTokenFromWeth: false});
            } else {
                tokenSwapTo = tokenOut;
            }
        }
        /// @dev Performs an action defined in `params` and returns the amount of `tokenSwapTo` received.
        function _performPoolAction(
            address tokenIn,
            uint256 amountIn,
            address tokenSwapTo,
            DefaultParams memory params
        ) internal returns (uint256 amountOut) {
            // Determine if we need to perform a swap
            if (params.action == Action.HandleEth) {
                // If no swap is required, amountOut doesn't change
                amountOut = amountIn;
            } else {
                // Record balance before the swap
                amountOut = IERC20(tokenSwapTo).balanceOf(address(this));
                // Approve the pool for spending exactly `amountIn` of `tokenIn`
                IERC20(tokenIn).safeIncreaseAllowance(params.pool, amountIn);
                if (params.action == Action.Swap) {
                    _swap(params.pool, params, amountIn, tokenSwapTo);
                } else if (params.action == Action.AddLiquidity) {
                    _addLiquidity(params.pool, params, amountIn, tokenSwapTo);
                } else {
                    // The only remaining action is RemoveLiquidity
                    _removeLiquidity(params.pool, params, amountIn, tokenSwapTo);
                }
                // Use the difference between the balance after the swap and the recorded balance as `amountOut`
                amountOut = IERC20(tokenSwapTo).balanceOf(address(this)) - amountOut;
            }
        }
        // ═══════════════════════════════════════ INTERNAL LOGIC: SWAP ACTIONS ════════════════════════════════════════════
        /// @dev Performs a swap through the given pool.
        /// Note: The pool should be already approved for spending `tokenIn`.
        function _swap(
            address pool,
            DefaultParams memory params,
            uint256 amountIn,
            address tokenOut
        ) internal {
            // tokenOut should match the "swap to" token
            if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert TokenAddressMismatch();
            // amountOut and deadline are not checked in RouterAdapter
            IDefaultPool(pool).swap({
                tokenIndexFrom: params.tokenIndexFrom,
                tokenIndexTo: params.tokenIndexTo,
                dx: amountIn,
                minDy: 0,
                deadline: type(uint256).max
            });
        }
        /// @dev Adds liquidity in a form of a single token to the given pool.
        /// Note: The pool should be already approved for spending `tokenIn`.
        function _addLiquidity(
            address pool,
            DefaultParams memory params,
            uint256 amountIn,
            address tokenOut
        ) internal {
            uint256 numTokens = _getPoolNumTokens(pool);
            address lpToken = _getPoolLPToken(pool);
            // tokenOut should match the LP token
            if (lpToken != tokenOut) revert TokenAddressMismatch();
            uint256[] memory amounts = new uint256[](numTokens);
            amounts[params.tokenIndexFrom] = amountIn;
            // amountOut and deadline are not checked in RouterAdapter
            IDefaultExtendedPool(pool).addLiquidity({amounts: amounts, minToMint: 0, deadline: type(uint256).max});
        }
        /// @dev Removes liquidity in a form of a single token from the given pool.
        /// Note: The pool should be already approved for spending `tokenIn`.
        function _removeLiquidity(
            address pool,
            DefaultParams memory params,
            uint256 amountIn,
            address tokenOut
        ) internal {
            // tokenOut should match the "swap to" token
            if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert TokenAddressMismatch();
            // amountOut and deadline are not checked in RouterAdapter
            IDefaultExtendedPool(pool).removeLiquidityOneToken({
                tokenAmount: amountIn,
                tokenIndex: params.tokenIndexTo,
                minAmount: 0,
                deadline: type(uint256).max
            });
        }
        // ═════════════════════════════════════════ INTERNAL LOGIC: POOL LENS ═════════════════════════════════════════════
        /// @dev Returns the LP token address of the given pool.
        function _getPoolLPToken(address pool) internal view returns (address lpToken) {
            (, , , , , , lpToken) = IDefaultExtendedPool(pool).swapStorage();
        }
        /// @dev Returns the number of tokens in the given pool.
        function _getPoolNumTokens(address pool) internal view returns (uint256 numTokens) {
            // Iterate over all tokens in the pool until the end is reached
            for (uint8 index = 0; ; ++index) {
                try IDefaultPool(pool).getToken(index) returns (address) {} catch {
                    // End of pool reached
                    numTokens = index;
                    break;
                }
            }
        }
        /// @dev Returns the tokens in the given pool.
        function _getPoolTokens(address pool) internal view returns (address[] memory tokens) {
            uint256 numTokens = _getPoolNumTokens(pool);
            tokens = new address[](numTokens);
            for (uint8 i = 0; i < numTokens; ++i) {
                // This will not revert because we already know the number of tokens in the pool
                tokens[i] = IDefaultPool(pool).getToken(i);
            }
        }
        /// @dev Returns the quote for a swap through the given pool.
        /// Note: will return 0 on invalid swaps.
        function _getPoolSwapQuote(
            address pool,
            uint8 tokenIndexFrom,
            uint8 tokenIndexTo,
            uint256 amountIn
        ) internal view returns (uint256 amountOut) {
            try IDefaultPool(pool).calculateSwap(tokenIndexFrom, tokenIndexTo, amountIn) returns (uint256 dy) {
                amountOut = dy;
            } catch {
                // Return 0 instead of reverting
                amountOut = 0;
            }
        }
        // ════════════════════════════════════════ INTERNAL LOGIC: ETH <> WETH ════════════════════════════════════════════
        /// @dev Wraps ETH into WETH.
        function _wrapETH(address weth, uint256 amount) internal {
            if (amount != msg.value) revert MsgValueIncorrect();
            // Deposit in order to have WETH in this contract
            IWETH9(weth).deposit{value: amount}();
        }
        /// @dev Unwraps WETH into ETH.
        function _unwrapETH(address weth, uint256 amount) internal {
            // Withdraw ETH to this contract
            IWETH9(weth).withdraw(amount);
        }
        /// @dev Derives WETH address from swap parameters.
        function _deriveWethAddress(
            address token,
            DefaultParams memory params,
            bool isTokenFromWeth
        ) internal view returns (address weth) {
            if (params.action == Action.HandleEth) {
                // If we only need to wrap/unwrap ETH, WETH address should be specified as the other token
                weth = token;
            } else {
                // Otherwise, we need to get WETH address from the liquidity pool
                weth = address(
                    IDefaultPool(params.pool).getToken(isTokenFromWeth ? params.tokenIndexFrom : params.tokenIndexTo)
                );
            }
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    interface IRouterAdapter {
        /// @notice Performs a tokenIn -> tokenOut swap, according to the provided params.
        /// If tokenIn is ETH_ADDRESS, this method should be invoked with `msg.value = amountIn`.
        /// If tokenIn is ERC20, the tokens should be already transferred to this contract (using `msg.value = 0`).
        /// If tokenOut is ETH_ADDRESS, native ETH will be sent to the recipient (be aware of potential reentrancy).
        /// If tokenOut is ERC20, the tokens will be transferred to the recipient.
        /// @dev Contracts implementing {IRouterAdapter} interface are required to enforce the above restrictions.
        /// On top of that, they must ensure that exactly `amountOut` worth of `tokenOut` is transferred to the recipient.
        /// Swap deadline and slippage is checked outside of this contract.
        /// @param recipient    Address to receive the swapped token
        /// @param tokenIn      Token to sell (use ETH_ADDRESS to start from native ETH)
        /// @param amountIn     Amount of tokens to sell
        /// @param tokenOut     Token to buy (use ETH_ADDRESS to end with native ETH)
        /// @param rawParams    Additional swap parameters
        /// @return amountOut   Amount of bought tokens
        function adapterSwap(
            address recipient,
            address tokenIn,
            uint256 amountIn,
            address tokenOut,
            bytes calldata rawParams
        ) external payable returns (uint256 amountOut);
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    error DeadlineExceeded();
    error InsufficientOutputAmount();
    error MsgValueIncorrect();
    error PoolNotFound();
    error TokenAddressMismatch();
    error TokenNotContract();
    error TokenNotETH();
    error TokensIdentical();
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts v4.4.1 (token/ERC20/utils/SafeERC20.sol)
    pragma solidity ^0.8.0;
    import "../IERC20.sol";
    import "../../../utils/Address.sol";
    /**
     * @title SafeERC20
     * @dev Wrappers around ERC20 operations that throw on failure (when the token
     * contract returns false). Tokens that return no value (and instead revert or
     * throw on failure) are also supported, non-reverting calls are assumed to be
     * successful.
     * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
     * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
     */
    library SafeERC20 {
        using Address for address;
        function safeTransfer(
            IERC20 token,
            address to,
            uint256 value
        ) internal {
            _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
        }
        function safeTransferFrom(
            IERC20 token,
            address from,
            address to,
            uint256 value
        ) internal {
            _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
        }
        /**
         * @dev Deprecated. This function has issues similar to the ones found in
         * {IERC20-approve}, and its usage is discouraged.
         *
         * Whenever possible, use {safeIncreaseAllowance} and
         * {safeDecreaseAllowance} instead.
         */
        function safeApprove(
            IERC20 token,
            address spender,
            uint256 value
        ) internal {
            // safeApprove should only be called when setting an initial allowance,
            // or when resetting it to zero. To increase and decrease it, use
            // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
            require(
                (value == 0) || (token.allowance(address(this), spender) == 0),
                "SafeERC20: approve from non-zero to non-zero allowance"
            );
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
        }
        function safeIncreaseAllowance(
            IERC20 token,
            address spender,
            uint256 value
        ) internal {
            uint256 newAllowance = token.allowance(address(this), spender) + value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
        function safeDecreaseAllowance(
            IERC20 token,
            address spender,
            uint256 value
        ) internal {
            unchecked {
                uint256 oldAllowance = token.allowance(address(this), spender);
                require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
                uint256 newAllowance = oldAllowance - value;
                _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
            }
        }
        /**
         * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
         * on the return value: the return value is optional (but if data is returned, it must not be false).
         * @param token The token targeted by the call.
         * @param data The call data (encoded using abi.encode or one of its variants).
         */
        function _callOptionalReturn(IERC20 token, bytes memory data) private {
            // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
            // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
            // the target address contains contract code and also asserts for success in the low-level call.
            bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
            if (returndata.length > 0) {
                // Return data is optional
                require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
            }
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
    pragma solidity ^0.8.0;
    /**
     * @dev Provides information about the current execution context, including the
     * sender of the transaction and its data. While these are generally available
     * via msg.sender and msg.data, they should not be accessed in such a direct
     * manner, since when dealing with meta-transactions the account sending and
     * paying for execution may not be the actual sender (as far as an application
     * is concerned).
     *
     * This contract is only required for intermediate, library-like contracts.
     */
    abstract contract Context {
        function _msgSender() internal view virtual returns (address) {
            return msg.sender;
        }
        function _msgData() internal view virtual returns (bytes calldata) {
            return msg.data;
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    import {IDefaultPool} from "./IDefaultPool.sol";
    interface IDefaultExtendedPool is IDefaultPool {
        function addLiquidity(
            uint256[] calldata amounts,
            uint256 minToMint,
            uint256 deadline
        ) external returns (uint256);
        function removeLiquidityOneToken(
            uint256 tokenAmount,
            uint8 tokenIndex,
            uint256 minAmount,
            uint256 deadline
        ) external returns (uint256);
        // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════
        function calculateRemoveLiquidity(uint256 amount) external view returns (uint256[] memory);
        function calculateRemoveLiquidityOneToken(uint256 tokenAmount, uint8 tokenIndex)
            external
            view
            returns (uint256 availableTokenAmount);
        function getAPrecise() external view returns (uint256);
        function getTokenBalance(uint8 index) external view returns (uint256);
        function swapStorage()
            external
            view
            returns (
                uint256 initialA,
                uint256 futureA,
                uint256 initialATime,
                uint256 futureATime,
                uint256 swapFee,
                uint256 adminFee,
                address lpToken
            );
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    interface IWETH9 {
        function deposit() external payable;
        function withdraw(uint256 wad) external;
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/IERC20.sol)
    pragma solidity ^0.8.0;
    /**
     * @dev Interface of the ERC20 standard as defined in the EIP.
     */
    interface IERC20 {
        /**
         * @dev Returns the amount of tokens in existence.
         */
        function totalSupply() external view returns (uint256);
        /**
         * @dev Returns the amount of tokens owned by `account`.
         */
        function balanceOf(address account) external view returns (uint256);
        /**
         * @dev Moves `amount` tokens from the caller's account to `to`.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transfer(address to, uint256 amount) external returns (bool);
        /**
         * @dev Returns the remaining number of tokens that `spender` will be
         * allowed to spend on behalf of `owner` through {transferFrom}. This is
         * zero by default.
         *
         * This value changes when {approve} or {transferFrom} are called.
         */
        function allowance(address owner, address spender) external view returns (uint256);
        /**
         * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * IMPORTANT: Beware that changing an allowance with this method brings the risk
         * that someone may use both the old and the new allowance by unfortunate
         * transaction ordering. One possible solution to mitigate this race
         * condition is to first reduce the spender's allowance to 0 and set the
         * desired value afterwards:
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
         *
         * Emits an {Approval} event.
         */
        function approve(address spender, uint256 amount) external returns (bool);
        /**
         * @dev Moves `amount` tokens from `from` to `to` using the
         * allowance mechanism. `amount` is then deducted from the caller's
         * allowance.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transferFrom(
            address from,
            address to,
            uint256 amount
        ) external returns (bool);
        /**
         * @dev Emitted when `value` tokens are moved from one account (`from`) to
         * another (`to`).
         *
         * Note that `value` may be zero.
         */
        event Transfer(address indexed from, address indexed to, uint256 value);
        /**
         * @dev Emitted when the allowance of a `spender` for an `owner` is set by
         * a call to {approve}. `value` is the new allowance.
         */
        event Approval(address indexed owner, address indexed spender, uint256 value);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v4.5.0) (utils/Address.sol)
    pragma solidity ^0.8.1;
    /**
     * @dev Collection of functions related to the address type
     */
    library Address {
        /**
         * @dev Returns true if `account` is a contract.
         *
         * [IMPORTANT]
         * ====
         * It is unsafe to assume that an address for which this function returns
         * false is an externally-owned account (EOA) and not a contract.
         *
         * Among others, `isContract` will return false for the following
         * types of addresses:
         *
         *  - an externally-owned account
         *  - a contract in construction
         *  - an address where a contract will be created
         *  - an address where a contract lived, but was destroyed
         * ====
         *
         * [IMPORTANT]
         * ====
         * You shouldn't rely on `isContract` to protect against flash loan attacks!
         *
         * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
         * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
         * constructor.
         * ====
         */
        function isContract(address account) internal view returns (bool) {
            // This method relies on extcodesize/address.code.length, which returns 0
            // for contracts in construction, since the code is only stored at the end
            // of the constructor execution.
            return account.code.length > 0;
        }
        /**
         * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
         * `recipient`, forwarding all available gas and reverting on errors.
         *
         * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
         * of certain opcodes, possibly making contracts go over the 2300 gas limit
         * imposed by `transfer`, making them unable to receive funds via
         * `transfer`. {sendValue} removes this limitation.
         *
         * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
         *
         * IMPORTANT: because control is transferred to `recipient`, care must be
         * taken to not create reentrancy vulnerabilities. Consider using
         * {ReentrancyGuard} or the
         * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
         */
        function sendValue(address payable recipient, uint256 amount) internal {
            require(address(this).balance >= amount, "Address: insufficient balance");
            (bool success, ) = recipient.call{value: amount}("");
            require(success, "Address: unable to send value, recipient may have reverted");
        }
        /**
         * @dev Performs a Solidity function call using a low level `call`. A
         * plain `call` is an unsafe replacement for a function call: use this
         * function instead.
         *
         * If `target` reverts with a revert reason, it is bubbled up by this
         * function (like regular Solidity function calls).
         *
         * Returns the raw returned data. To convert to the expected return value,
         * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
         *
         * Requirements:
         *
         * - `target` must be a contract.
         * - calling `target` with `data` must not revert.
         *
         * _Available since v3.1._
         */
        function functionCall(address target, bytes memory data) internal returns (bytes memory) {
            return functionCall(target, data, "Address: low-level call failed");
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
         * `errorMessage` as a fallback revert reason when `target` reverts.
         *
         * _Available since v3.1._
         */
        function functionCall(
            address target,
            bytes memory data,
            string memory errorMessage
        ) internal returns (bytes memory) {
            return functionCallWithValue(target, data, 0, errorMessage);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but also transferring `value` wei to `target`.
         *
         * Requirements:
         *
         * - the calling contract must have an ETH balance of at least `value`.
         * - the called Solidity function must be `payable`.
         *
         * _Available since v3.1._
         */
        function functionCallWithValue(
            address target,
            bytes memory data,
            uint256 value
        ) internal returns (bytes memory) {
            return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
        }
        /**
         * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
         * with `errorMessage` as a fallback revert reason when `target` reverts.
         *
         * _Available since v3.1._
         */
        function functionCallWithValue(
            address target,
            bytes memory data,
            uint256 value,
            string memory errorMessage
        ) internal returns (bytes memory) {
            require(address(this).balance >= value, "Address: insufficient balance for call");
            require(isContract(target), "Address: call to non-contract");
            (bool success, bytes memory returndata) = target.call{value: value}(data);
            return verifyCallResult(success, returndata, errorMessage);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but performing a static call.
         *
         * _Available since v3.3._
         */
        function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
            return functionStaticCall(target, data, "Address: low-level static call failed");
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
         * but performing a static call.
         *
         * _Available since v3.3._
         */
        function functionStaticCall(
            address target,
            bytes memory data,
            string memory errorMessage
        ) internal view returns (bytes memory) {
            require(isContract(target), "Address: static call to non-contract");
            (bool success, bytes memory returndata) = target.staticcall(data);
            return verifyCallResult(success, returndata, errorMessage);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but performing a delegate call.
         *
         * _Available since v3.4._
         */
        function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
            return functionDelegateCall(target, data, "Address: low-level delegate call failed");
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
         * but performing a delegate call.
         *
         * _Available since v3.4._
         */
        function functionDelegateCall(
            address target,
            bytes memory data,
            string memory errorMessage
        ) internal returns (bytes memory) {
            require(isContract(target), "Address: delegate call to non-contract");
            (bool success, bytes memory returndata) = target.delegatecall(data);
            return verifyCallResult(success, returndata, errorMessage);
        }
        /**
         * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
         * revert reason using the provided one.
         *
         * _Available since v4.3._
         */
        function verifyCallResult(
            bool success,
            bytes memory returndata,
            string memory errorMessage
        ) internal pure returns (bytes memory) {
            if (success) {
                return returndata;
            } else {
                // Look for revert reason and bubble it up if present
                if (returndata.length > 0) {
                    // The easiest way to bubble the revert reason is using memory via assembly
                    assembly {
                        let returndata_size := mload(returndata)
                        revert(add(32, returndata), returndata_size)
                    }
                } else {
                    revert(errorMessage);
                }
            }
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;
    interface IDefaultPool {
        function swap(
            uint8 tokenIndexFrom,
            uint8 tokenIndexTo,
            uint256 dx,
            uint256 minDy,
            uint256 deadline
        ) external returns (uint256 amountOut);
        function calculateSwap(
            uint8 tokenIndexFrom,
            uint8 tokenIndexTo,
            uint256 dx
        ) external view returns (uint256 amountOut);
        function getToken(uint8 index) external view returns (address token);
    }
    

    File 2 of 2: FastBridge
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    import "./libs/Errors.sol";
    import {UniversalTokenLib} from "./libs/UniversalToken.sol";
    import {Admin} from "./Admin.sol";
    import {IFastBridge} from "./interfaces/IFastBridge.sol";
    contract FastBridge is IFastBridge, Admin {
        using SafeERC20 for IERC20;
        using UniversalTokenLib for address;
        /// @notice Dispute period for relayed transactions
        uint256 public constant DISPUTE_PERIOD = 30 minutes;
        /// @notice Delay for a transaction after which it could be permisionlessly refunded
        uint256 public constant REFUND_DELAY = 7 days;
        /// @notice Minimum deadline period to relay a requested bridge transaction
        uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes;
        enum BridgeStatus {
            NULL, // doesn't exist yet
            REQUESTED,
            RELAYER_PROVED,
            RELAYER_CLAIMED,
            REFUNDED
        }
        /// @notice Status of the bridge tx on origin chain
        mapping(bytes32 => BridgeStatus) public bridgeStatuses;
        /// @notice Proof of relayed bridge tx on origin chain
        mapping(bytes32 => BridgeProof) public bridgeProofs;
        /// @notice Whether bridge has been relayed on destination chain
        mapping(bytes32 => bool) public bridgeRelays;
        /// @dev to prevent replays
        uint256 public nonce;
        // @dev the block the contract was deployed at
        uint256 public immutable deployBlock;
        constructor(address _owner) Admin(_owner) {
            deployBlock = block.number;
        }
        /// @notice Pulls a requested token from the user to the requested recipient.
        /// @dev Be careful of re-entrancy issues when msg.value > 0 and recipient != address(this)
        function _pullToken(address recipient, address token, uint256 amount) internal returns (uint256 amountPulled) {
            if (token != UniversalTokenLib.ETH_ADDRESS) {
                token.assertIsContract();
                // Record token balance before transfer
                amountPulled = IERC20(token).balanceOf(recipient);
                // Token needs to be pulled only if msg.value is zero
                // This way user can specify WETH as the origin asset
                IERC20(token).safeTransferFrom(msg.sender, recipient, amount);
                // Use the difference between the recorded balance and the current balance as the amountPulled
                amountPulled = IERC20(token).balanceOf(recipient) - amountPulled;
            } else {
                // Otherwise, we need to check that ETH amount matches msg.value
                if (amount != msg.value) revert MsgValueIncorrect();
                // Transfer value to recipient if not this address
                if (recipient != address(this)) token.universalTransfer(recipient, amount);
                // We will forward msg.value in the external call later, if recipient is not this contract
                amountPulled = msg.value;
            }
        }
        /// @inheritdoc IFastBridge
        function getBridgeTransaction(bytes memory request) public pure returns (BridgeTransaction memory) {
            return abi.decode(request, (BridgeTransaction));
        }
        /// @inheritdoc IFastBridge
        function bridge(BridgeParams memory params) external payable {
            // check bridge params
            if (params.dstChainId == block.chainid) revert ChainIncorrect();
            if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect();
            if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress();
            if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort();
            // transfer tokens to bridge contract
            // @dev use returned originAmount in request in case of transfer fees
            uint256 originAmount = _pullToken(address(this), params.originToken, params.originAmount);
            // track amount of origin token owed to protocol
            uint256 originFeeAmount;
            if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS;
            originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers
            // set status to requested
            bytes memory request = abi.encode(
                BridgeTransaction({
                    originChainId: uint32(block.chainid),
                    destChainId: params.dstChainId,
                    originSender: params.sender,
                    destRecipient: params.to,
                    originToken: params.originToken,
                    destToken: params.destToken,
                    originAmount: originAmount,
                    destAmount: params.destAmount,
                    originFeeAmount: originFeeAmount,
                    sendChainGas: params.sendChainGas,
                    deadline: params.deadline,
                    nonce: nonce++ // increment nonce on every bridge
                })
            );
            bytes32 transactionId = keccak256(request);
            bridgeStatuses[transactionId] = BridgeStatus.REQUESTED;
            emit BridgeRequested(
                transactionId,
                params.sender,
                request,
                params.dstChainId,
                params.originToken,
                params.destToken,
                originAmount,
                params.destAmount,
                params.sendChainGas
            );
        }
        /// @inheritdoc IFastBridge
        function relay(bytes memory request) external payable onlyRole(RELAYER_ROLE) {
            bytes32 transactionId = keccak256(request);
            BridgeTransaction memory transaction = getBridgeTransaction(request);
            if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect();
            // check haven't exceeded deadline for relay to happen
            if (block.timestamp > transaction.deadline) revert DeadlineExceeded();
            // mark bridge transaction as relayed
            if (bridgeRelays[transactionId]) revert TransactionRelayed();
            bridgeRelays[transactionId] = true;
            // transfer tokens to recipient on destination chain and gas rebate if requested
            address to = transaction.destRecipient;
            address token = transaction.destToken;
            uint256 amount = transaction.destAmount;
            uint256 rebate = chainGasAmount;
            if (!transaction.sendChainGas) {
                // forward erc20
                rebate = 0;
                _pullToken(to, token, amount);
            } else if (token == UniversalTokenLib.ETH_ADDRESS) {
                // lump in gas rebate into amount in native gas token
                _pullToken(to, token, amount + rebate);
            } else {
                // forward erc20 then forward gas rebate in native gas token
                _pullToken(to, token, amount);
                _pullToken(to, UniversalTokenLib.ETH_ADDRESS, rebate);
            }
            emit BridgeRelayed(
                transactionId,
                msg.sender,
                to,
                transaction.originChainId,
                transaction.originToken,
                transaction.destToken,
                transaction.originAmount,
                transaction.destAmount,
                rebate
            );
        }
        /// @inheritdoc IFastBridge
        function prove(bytes memory request, bytes32 destTxHash) external onlyRole(RELAYER_ROLE) {
            bytes32 transactionId = keccak256(request);
            // update bridge tx status given proof provided
            if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect();
            bridgeStatuses[transactionId] = BridgeStatus.RELAYER_PROVED;
            bridgeProofs[transactionId] = BridgeProof({timestamp: uint96(block.timestamp), relayer: msg.sender}); // overflow ok
            emit BridgeProofProvided(transactionId, msg.sender, destTxHash);
        }
        /// @notice Calculates time since proof submitted
        /// @dev proof.timestamp stores casted uint96(block.timestamp) block timestamps for gas optimization
        ///      _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint96).max but
        ///      proof.timestamp < type(uint96).max via unchecked statement
        /// @param proof The bridge proof
        /// @return delta Time delta since proof submitted
        function _timeSince(BridgeProof memory proof) internal view returns (uint256 delta) {
            unchecked {
                delta = uint96(block.timestamp) - proof.timestamp;
            }
        }
        /// @inheritdoc IFastBridge
        function canClaim(bytes32 transactionId, address relayer) external view returns (bool) {
            if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
            BridgeProof memory proof = bridgeProofs[transactionId];
            if (proof.relayer != relayer) revert SenderIncorrect();
            return _timeSince(proof) > DISPUTE_PERIOD;
        }
        /// @inheritdoc IFastBridge
        function claim(bytes memory request, address to) external onlyRole(RELAYER_ROLE) {
            bytes32 transactionId = keccak256(request);
            BridgeTransaction memory transaction = getBridgeTransaction(request);
            // update bridge tx status if able to claim origin collateral
            if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
            BridgeProof memory proof = bridgeProofs[transactionId];
            if (proof.relayer != msg.sender) revert SenderIncorrect();
            if (_timeSince(proof) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed();
            bridgeStatuses[transactionId] = BridgeStatus.RELAYER_CLAIMED;
            // update protocol fees if origin fee amount exists
            if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount;
            // transfer origin collateral less fee to specified address
            address token = transaction.originToken;
            uint256 amount = transaction.originAmount;
            token.universalTransfer(to, amount);
            emit BridgeDepositClaimed(transactionId, msg.sender, to, token, amount);
        }
        /// @inheritdoc IFastBridge
        function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) {
            if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
            if (_timeSince(bridgeProofs[transactionId]) > DISPUTE_PERIOD) revert DisputePeriodPassed();
            // @dev relayer gets slashed effectively if dest relay has gone thru
            bridgeStatuses[transactionId] = BridgeStatus.REQUESTED;
            delete bridgeProofs[transactionId];
            emit BridgeProofDisputed(transactionId, msg.sender);
        }
        /// @inheritdoc IFastBridge
        function refund(bytes memory request) external {
            bytes32 transactionId = keccak256(request);
            BridgeTransaction memory transaction = getBridgeTransaction(request);
            if (hasRole(REFUNDER_ROLE, msg.sender)) {
                // Refunder can refund if deadline has passed
                if (block.timestamp <= transaction.deadline) revert DeadlineNotExceeded();
            } else {
                // Permissionless refund is allowed after REFUND_DELAY
                if (block.timestamp <= transaction.deadline + REFUND_DELAY) revert DeadlineNotExceeded();
            }
            // set status to refunded if still in requested state
            if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect();
            bridgeStatuses[transactionId] = BridgeStatus.REFUNDED;
            // transfer origin collateral back to original sender
            address to = transaction.originSender;
            address token = transaction.originToken;
            uint256 amount = transaction.originAmount + transaction.originFeeAmount;
            token.universalTransfer(to, amount);
            emit BridgeDepositRefunded(transactionId, to, token, amount);
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol)
    pragma solidity ^0.8.20;
    import {IERC20} from "../IERC20.sol";
    import {IERC20Permit} from "../extensions/IERC20Permit.sol";
    import {Address} from "../../../utils/Address.sol";
    /**
     * @title SafeERC20
     * @dev Wrappers around ERC-20 operations that throw on failure (when the token
     * contract returns false). Tokens that return no value (and instead revert or
     * throw on failure) are also supported, non-reverting calls are assumed to be
     * successful.
     * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
     * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
     */
    library SafeERC20 {
        using Address for address;
        /**
         * @dev An operation with an ERC-20 token failed.
         */
        error SafeERC20FailedOperation(address token);
        /**
         * @dev Indicates a failed `decreaseAllowance` request.
         */
        error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
        /**
         * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
         * non-reverting calls are assumed to be successful.
         */
        function safeTransfer(IERC20 token, address to, uint256 value) internal {
            _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
        }
        /**
         * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
         * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
         */
        function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
            _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
        }
        /**
         * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
         * non-reverting calls are assumed to be successful.
         */
        function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
            uint256 oldAllowance = token.allowance(address(this), spender);
            forceApprove(token, spender, oldAllowance + value);
        }
        /**
         * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
         * value, non-reverting calls are assumed to be successful.
         */
        function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
            unchecked {
                uint256 currentAllowance = token.allowance(address(this), spender);
                if (currentAllowance < requestedDecrease) {
                    revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
                }
                forceApprove(token, spender, currentAllowance - requestedDecrease);
            }
        }
        /**
         * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
         * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
         * to be set to zero before setting it to a non-zero value, such as USDT.
         */
        function forceApprove(IERC20 token, address spender, uint256 value) internal {
            bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
            if (!_callOptionalReturnBool(token, approvalCall)) {
                _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
                _callOptionalReturn(token, approvalCall);
            }
        }
        /**
         * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
         * on the return value: the return value is optional (but if data is returned, it must not be false).
         * @param token The token targeted by the call.
         * @param data The call data (encoded using abi.encode or one of its variants).
         */
        function _callOptionalReturn(IERC20 token, bytes memory data) private {
            // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
            // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
            // the target address contains contract code and also asserts for success in the low-level call.
            bytes memory returndata = address(token).functionCall(data);
            if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
                revert SafeERC20FailedOperation(address(token));
            }
        }
        /**
         * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
         * on the return value: the return value is optional (but if data is returned, it must not be false).
         * @param token The token targeted by the call.
         * @param data The call data (encoded using abi.encode or one of its variants).
         *
         * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
         */
        function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
            // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
            // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
            // and not revert is the subcall reverts.
            (bool success, bytes memory returndata) = address(token).call(data);
            return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    error DeadlineExceeded();
    error DeadlineNotExceeded();
    error DeadlineTooShort();
    error InsufficientOutputAmount();
    error MsgValueIncorrect();
    error PoolNotFound();
    error TokenAddressMismatch();
    error TokenNotContract();
    error TokenNotETH();
    error TokensIdentical();
    error ChainIncorrect();
    error AmountIncorrect();
    error ZeroAddress();
    error DisputePeriodNotPassed();
    error DisputePeriodPassed();
    error SenderIncorrect();
    error StatusIncorrect();
    error TransactionIdIncorrect();
    error TransactionRelayed();
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    import {TokenNotContract} from "./Errors.sol";
    import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    library UniversalTokenLib {
        using SafeERC20 for IERC20;
        address internal constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
        /// @notice Transfers tokens to the given account. Reverts if transfer is not successful.
        /// @dev This might trigger fallback, if ETH is transferred to the contract.
        /// Make sure this can not lead to reentrancy attacks.
        function universalTransfer(address token, address to, uint256 value) internal {
            // Don't do anything, if need to send tokens to this address
            if (to == address(this)) return;
            // Don't do anything, if trying to send zero value
            if (value == 0) return;
            if (token == ETH_ADDRESS) {
                /// @dev Note: this can potentially lead to executing code in `to`.
                // solhint-disable-next-line avoid-low-level-calls
                (bool success,) = to.call{value: value}("");
                require(success, "ETH transfer failed");
            } else {
                IERC20(token).safeTransfer(to, value);
            }
        }
        /// @notice Issues an infinite allowance to the spender, if the current allowance is insufficient
        /// to spend the given amount.
        function universalApproveInfinity(address token, address spender, uint256 amountToSpend) internal {
            // ETH Chad doesn't require your approval
            if (token == ETH_ADDRESS) return;
            // No-op if allowance is already sufficient
            uint256 allowance = IERC20(token).allowance(address(this), spender);
            if (allowance >= amountToSpend) return;
            // Otherwise, reset approval to 0 and set to max allowance
            if (allowance > 0) IERC20(token).safeDecreaseAllowance(spender, allowance);
            IERC20(token).safeIncreaseAllowance(spender, type(uint256).max);
        }
        /// @notice Returns the balance of the given token (or native ETH) for the given account.
        function universalBalanceOf(address token, address account) internal view returns (uint256) {
            if (token == ETH_ADDRESS) {
                return account.balance;
            } else {
                return IERC20(token).balanceOf(account);
            }
        }
        /// @dev Checks that token is a contract and not ETH_ADDRESS.
        function assertIsContract(address token) internal view {
            // Check that ETH_ADDRESS was not used (in case this is a predeploy on any of the chains)
            if (token == UniversalTokenLib.ETH_ADDRESS) revert TokenNotContract();
            // Check that token is not an EOA
            if (token.code.length == 0) revert TokenNotContract();
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.20;
    import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
    import {UniversalTokenLib} from "./libs/UniversalToken.sol";
    import {IAdmin} from "./interfaces/IAdmin.sol";
    contract Admin is IAdmin, AccessControlEnumerable {
        using UniversalTokenLib for address;
        bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
        bytes32 public constant REFUNDER_ROLE = keccak256("REFUNDER_ROLE");
        bytes32 public constant GUARD_ROLE = keccak256("GUARD_ROLE");
        bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
        uint256 public constant FEE_BPS = 1e6;
        uint256 public constant FEE_RATE_MAX = 0.01e6; // max 1% on origin amount
        /// @notice Protocol fee rate taken on origin amount deposited in origin chain
        uint256 public protocolFeeRate;
        /// @notice Protocol fee amounts accumulated
        mapping(address => uint256) public protocolFees;
        /// @notice Chain gas amount to forward as rebate if requested
        uint256 public chainGasAmount;
        constructor(address _owner) {
            _grantRole(DEFAULT_ADMIN_ROLE, _owner);
        }
        function setProtocolFeeRate(uint256 newFeeRate) external onlyRole(GOVERNOR_ROLE) {
            require(newFeeRate <= FEE_RATE_MAX, "newFeeRate > max");
            uint256 oldFeeRate = protocolFeeRate;
            protocolFeeRate = newFeeRate;
            emit FeeRateUpdated(oldFeeRate, newFeeRate);
        }
        function sweepProtocolFees(address token, address recipient) external onlyRole(GOVERNOR_ROLE) {
            uint256 feeAmount = protocolFees[token];
            if (feeAmount == 0) return; // skip if no accumulated fees
            protocolFees[token] = 0;
            token.universalTransfer(recipient, feeAmount);
            emit FeesSwept(token, recipient, feeAmount);
        }
        function setChainGasAmount(uint256 newChainGasAmount) external onlyRole(GOVERNOR_ROLE) {
            uint256 oldChainGasAmount = chainGasAmount;
            chainGasAmount = newChainGasAmount;
            emit ChainGasAmountUpdated(oldChainGasAmount, newChainGasAmount);
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    interface IFastBridge {
        struct BridgeTransaction {
            uint32 originChainId;
            uint32 destChainId;
            address originSender; // user (origin)
            address destRecipient; // user (dest)
            address originToken;
            address destToken;
            uint256 originAmount; // amount in on origin bridge less originFeeAmount
            uint256 destAmount;
            uint256 originFeeAmount;
            bool sendChainGas;
            uint256 deadline; // user specified deadline for destination relay
            uint256 nonce;
        }
        struct BridgeProof {
            uint96 timestamp;
            address relayer;
        }
        // ============ Events ============
        event BridgeRequested(
            bytes32 indexed transactionId,
            address indexed sender,
            bytes request,
            uint32 destChainId,
            address originToken,
            address destToken,
            uint256 originAmount,
            uint256 destAmount,
            bool sendChainGas
        );
        event BridgeRelayed(
            bytes32 indexed transactionId,
            address indexed relayer,
            address indexed to,
            uint32 originChainId,
            address originToken,
            address destToken,
            uint256 originAmount,
            uint256 destAmount,
            uint256 chainGasAmount
        );
        event BridgeProofProvided(bytes32 indexed transactionId, address indexed relayer, bytes32 transactionHash);
        event BridgeProofDisputed(bytes32 indexed transactionId, address indexed relayer);
        event BridgeDepositClaimed(
            bytes32 indexed transactionId, address indexed relayer, address indexed to, address token, uint256 amount
        );
        event BridgeDepositRefunded(bytes32 indexed transactionId, address indexed to, address token, uint256 amount);
        // ============ Methods ============
        struct BridgeParams {
            uint32 dstChainId;
            address sender;
            address to;
            address originToken;
            address destToken;
            uint256 originAmount; // should include protocol fee (if any)
            uint256 destAmount; // should include relayer fee
            bool sendChainGas;
            uint256 deadline;
        }
        /// @notice Initiates bridge on origin chain to be relayed by off-chain relayer
        /// @param params The parameters required to bridge
        function bridge(BridgeParams memory params) external payable;
        /// @notice Relays destination side of bridge transaction by off-chain relayer
        /// @param request The encoded bridge transaction to relay on destination chain
        function relay(bytes memory request) external payable;
        /// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction
        /// @param request The encoded bridge transaction to prove on origin chain
        /// @param destTxHash The destination tx hash proving bridge transaction was relayed
        function prove(bytes memory request, bytes32 destTxHash) external;
        /// @notice Completes bridge transaction on origin chain by claiming originally deposited capital
        /// @param request The encoded bridge transaction to claim on origin chain
        /// @param to The recipient address of the funds
        function claim(bytes memory request, address to) external;
        /// @notice Disputes an outstanding proof in case relayer provided dest chain tx is invalid
        /// @param transactionId The transaction id associated with the encoded bridge transaction to dispute
        function dispute(bytes32 transactionId) external;
        /// @notice Refunds an outstanding bridge transaction in case optimistic bridging failed
        /// @param request The encoded bridge transaction to refund
        function refund(bytes memory request) external;
        // ============ Views ============
        /// @notice Decodes bridge request into a bridge transaction
        /// @param request The bridge request to decode
        function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory);
        /// @notice Checks if the dispute period has passed so bridge deposit can be claimed
        /// @param transactionId The transaction id associated with the encoded bridge transaction to check
        /// @param relayer The address of the relayer attempting to claim
        function canClaim(bytes32 transactionId, address relayer) external view returns (bool);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Interface of the ERC-20 standard as defined in the ERC.
     */
    interface IERC20 {
        /**
         * @dev Emitted when `value` tokens are moved from one account (`from`) to
         * another (`to`).
         *
         * Note that `value` may be zero.
         */
        event Transfer(address indexed from, address indexed to, uint256 value);
        /**
         * @dev Emitted when the allowance of a `spender` for an `owner` is set by
         * a call to {approve}. `value` is the new allowance.
         */
        event Approval(address indexed owner, address indexed spender, uint256 value);
        /**
         * @dev Returns the value of tokens in existence.
         */
        function totalSupply() external view returns (uint256);
        /**
         * @dev Returns the value of tokens owned by `account`.
         */
        function balanceOf(address account) external view returns (uint256);
        /**
         * @dev Moves a `value` amount of tokens from the caller's account to `to`.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transfer(address to, uint256 value) external returns (bool);
        /**
         * @dev Returns the remaining number of tokens that `spender` will be
         * allowed to spend on behalf of `owner` through {transferFrom}. This is
         * zero by default.
         *
         * This value changes when {approve} or {transferFrom} are called.
         */
        function allowance(address owner, address spender) external view returns (uint256);
        /**
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
         * caller's tokens.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * IMPORTANT: Beware that changing an allowance with this method brings the risk
         * that someone may use both the old and the new allowance by unfortunate
         * transaction ordering. One possible solution to mitigate this race
         * condition is to first reduce the spender's allowance to 0 and set the
         * desired value afterwards:
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
         *
         * Emits an {Approval} event.
         */
        function approve(address spender, uint256 value) external returns (bool);
        /**
         * @dev Moves a `value` amount of tokens from `from` to `to` using the
         * allowance mechanism. `value` is then deducted from the caller's
         * allowance.
         *
         * Returns a boolean value indicating whether the operation succeeded.
         *
         * Emits a {Transfer} event.
         */
        function transferFrom(address from, address to, uint256 value) external returns (bool);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
     * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
     *
     * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
     * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
     * need to send a transaction, and thus is not required to hold Ether at all.
     *
     * ==== Security Considerations
     *
     * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature
     * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be
     * considered as an intention to spend the allowance in any specific way. The second is that because permits have
     * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should
     * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be
     * generally recommended is:
     *
     * ```solidity
     * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {
     *     try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}
     *     doThing(..., value);
     * }
     *
     * function doThing(..., uint256 value) public {
     *     token.safeTransferFrom(msg.sender, address(this), value);
     *     ...
     * }
     * ```
     *
     * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of
     * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also
     * {SafeERC20-safeTransferFrom}).
     *
     * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so
     * contracts should have entry points that don't rely on permit.
     */
    interface IERC20Permit {
        /**
         * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
         * given ``owner``'s signed approval.
         *
         * IMPORTANT: The same issues {IERC20-approve} has related to transaction
         * ordering also apply here.
         *
         * Emits an {Approval} event.
         *
         * Requirements:
         *
         * - `spender` cannot be the zero address.
         * - `deadline` must be a timestamp in the future.
         * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
         * over the EIP712-formatted function arguments.
         * - the signature must use ``owner``'s current nonce (see {nonces}).
         *
         * For more information on the signature format, see the
         * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
         * section].
         *
         * CAUTION: See Security Considerations above.
         */
        function permit(
            address owner,
            address spender,
            uint256 value,
            uint256 deadline,
            uint8 v,
            bytes32 r,
            bytes32 s
        ) external;
        /**
         * @dev Returns the current nonce for `owner`. This value must be
         * included whenever a signature is generated for {permit}.
         *
         * Every successful call to {permit} increases ``owner``'s nonce by one. This
         * prevents a signature from being used multiple times.
         */
        function nonces(address owner) external view returns (uint256);
        /**
         * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
         */
        // solhint-disable-next-line func-name-mixedcase
        function DOMAIN_SEPARATOR() external view returns (bytes32);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Collection of functions related to the address type
     */
    library Address {
        /**
         * @dev The ETH balance of the account is not enough to perform the operation.
         */
        error AddressInsufficientBalance(address account);
        /**
         * @dev There's no code at `target` (it is not a contract).
         */
        error AddressEmptyCode(address target);
        /**
         * @dev A call to an address target failed. The target may have reverted.
         */
        error FailedInnerCall();
        /**
         * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
         * `recipient`, forwarding all available gas and reverting on errors.
         *
         * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
         * of certain opcodes, possibly making contracts go over the 2300 gas limit
         * imposed by `transfer`, making them unable to receive funds via
         * `transfer`. {sendValue} removes this limitation.
         *
         * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
         *
         * IMPORTANT: because control is transferred to `recipient`, care must be
         * taken to not create reentrancy vulnerabilities. Consider using
         * {ReentrancyGuard} or the
         * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
         */
        function sendValue(address payable recipient, uint256 amount) internal {
            if (address(this).balance < amount) {
                revert AddressInsufficientBalance(address(this));
            }
            (bool success, ) = recipient.call{value: amount}("");
            if (!success) {
                revert FailedInnerCall();
            }
        }
        /**
         * @dev Performs a Solidity function call using a low level `call`. A
         * plain `call` is an unsafe replacement for a function call: use this
         * function instead.
         *
         * If `target` reverts with a revert reason or custom error, it is bubbled
         * up by this function (like regular Solidity function calls). However, if
         * the call reverted with no returned reason, this function reverts with a
         * {FailedInnerCall} error.
         *
         * Returns the raw returned data. To convert to the expected return value,
         * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
         *
         * Requirements:
         *
         * - `target` must be a contract.
         * - calling `target` with `data` must not revert.
         */
        function functionCall(address target, bytes memory data) internal returns (bytes memory) {
            return functionCallWithValue(target, data, 0);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but also transferring `value` wei to `target`.
         *
         * Requirements:
         *
         * - the calling contract must have an ETH balance of at least `value`.
         * - the called Solidity function must be `payable`.
         */
        function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
            if (address(this).balance < value) {
                revert AddressInsufficientBalance(address(this));
            }
            (bool success, bytes memory returndata) = target.call{value: value}(data);
            return verifyCallResultFromTarget(target, success, returndata);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but performing a static call.
         */
        function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
            (bool success, bytes memory returndata) = target.staticcall(data);
            return verifyCallResultFromTarget(target, success, returndata);
        }
        /**
         * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
         * but performing a delegate call.
         */
        function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
            (bool success, bytes memory returndata) = target.delegatecall(data);
            return verifyCallResultFromTarget(target, success, returndata);
        }
        /**
         * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
         * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an
         * unsuccessful call.
         */
        function verifyCallResultFromTarget(
            address target,
            bool success,
            bytes memory returndata
        ) internal view returns (bytes memory) {
            if (!success) {
                _revert(returndata);
            } else {
                // only check if target is a contract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                if (returndata.length == 0 && target.code.length == 0) {
                    revert AddressEmptyCode(target);
                }
                return returndata;
            }
        }
        /**
         * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the
         * revert reason or with a default {FailedInnerCall} error.
         */
        function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
            if (!success) {
                _revert(returndata);
            } else {
                return returndata;
            }
        }
        /**
         * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}.
         */
        function _revert(bytes memory returndata) private pure {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly
                /// @solidity memory-safe-assembly
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert FailedInnerCall();
            }
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/AccessControlEnumerable.sol)
    pragma solidity ^0.8.20;
    import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol";
    import {AccessControl} from "../AccessControl.sol";
    import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
    /**
     * @dev Extension of {AccessControl} that allows enumerating the members of each role.
     */
    abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {
        using EnumerableSet for EnumerableSet.AddressSet;
        mapping(bytes32 role => EnumerableSet.AddressSet) private _roleMembers;
        /**
         * @dev See {IERC165-supportsInterface}.
         */
        function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
            return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
        }
        /**
         * @dev Returns one of the accounts that have `role`. `index` must be a
         * value between 0 and {getRoleMemberCount}, non-inclusive.
         *
         * Role bearers are not sorted in any particular way, and their ordering may
         * change at any point.
         *
         * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
         * you perform all queries on the same block. See the following
         * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
         * for more information.
         */
        function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
            return _roleMembers[role].at(index);
        }
        /**
         * @dev Returns the number of accounts that have `role`. Can be used
         * together with {getRoleMember} to enumerate all bearers of a role.
         */
        function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
            return _roleMembers[role].length();
        }
        /**
         * @dev Overload {AccessControl-_grantRole} to track enumerable memberships
         */
        function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
            bool granted = super._grantRole(role, account);
            if (granted) {
                _roleMembers[role].add(account);
            }
            return granted;
        }
        /**
         * @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
         */
        function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
            bool revoked = super._revokeRole(role, account);
            if (revoked) {
                _roleMembers[role].remove(account);
            }
            return revoked;
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    interface IAdmin {
        // ============ Events ============
        event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate);
        event FeesSwept(address token, address recipient, uint256 amount);
        event ChainGasAmountUpdated(uint256 oldChainGasAmount, uint256 newChainGasAmount);
        // ============ Methods ============
        function setProtocolFeeRate(uint256 newFeeRate) external;
        function sweepProtocolFees(address token, address recipient) external;
        function setChainGasAmount(uint256 newChainGasAmount) external;
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/IAccessControlEnumerable.sol)
    pragma solidity ^0.8.20;
    import {IAccessControl} from "../IAccessControl.sol";
    /**
     * @dev External interface of AccessControlEnumerable declared to support ERC-165 detection.
     */
    interface IAccessControlEnumerable is IAccessControl {
        /**
         * @dev Returns one of the accounts that have `role`. `index` must be a
         * value between 0 and {getRoleMemberCount}, non-inclusive.
         *
         * Role bearers are not sorted in any particular way, and their ordering may
         * change at any point.
         *
         * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
         * you perform all queries on the same block. See the following
         * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
         * for more information.
         */
        function getRoleMember(bytes32 role, uint256 index) external view returns (address);
        /**
         * @dev Returns the number of accounts that have `role`. Can be used
         * together with {getRoleMember} to enumerate all bearers of a role.
         */
        function getRoleMemberCount(bytes32 role) external view returns (uint256);
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
    pragma solidity ^0.8.20;
    import {IAccessControl} from "./IAccessControl.sol";
    import {Context} from "../utils/Context.sol";
    import {ERC165} from "../utils/introspection/ERC165.sol";
    /**
     * @dev Contract module that allows children to implement role-based access
     * control mechanisms. This is a lightweight version that doesn't allow enumerating role
     * members except through off-chain means by accessing the contract event logs. Some
     * applications may benefit from on-chain enumerability, for those cases see
     * {AccessControlEnumerable}.
     *
     * Roles are referred to by their `bytes32` identifier. These should be exposed
     * in the external API and be unique. The best way to achieve this is by
     * using `public constant` hash digests:
     *
     * ```solidity
     * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
     * ```
     *
     * Roles can be used to represent a set of permissions. To restrict access to a
     * function call, use {hasRole}:
     *
     * ```solidity
     * function foo() public {
     *     require(hasRole(MY_ROLE, msg.sender));
     *     ...
     * }
     * ```
     *
     * Roles can be granted and revoked dynamically via the {grantRole} and
     * {revokeRole} functions. Each role has an associated admin role, and only
     * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
     *
     * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
     * that only accounts with this role will be able to grant or revoke other
     * roles. More complex role relationships can be created by using
     * {_setRoleAdmin}.
     *
     * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
     * grant and revoke this role. Extra precautions should be taken to secure
     * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
     * to enforce additional security measures for this role.
     */
    abstract contract AccessControl is Context, IAccessControl, ERC165 {
        struct RoleData {
            mapping(address account => bool) hasRole;
            bytes32 adminRole;
        }
        mapping(bytes32 role => RoleData) private _roles;
        bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
        /**
         * @dev Modifier that checks that an account has a specific role. Reverts
         * with an {AccessControlUnauthorizedAccount} error including the required role.
         */
        modifier onlyRole(bytes32 role) {
            _checkRole(role);
            _;
        }
        /**
         * @dev See {IERC165-supportsInterface}.
         */
        function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
            return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
        }
        /**
         * @dev Returns `true` if `account` has been granted `role`.
         */
        function hasRole(bytes32 role, address account) public view virtual returns (bool) {
            return _roles[role].hasRole[account];
        }
        /**
         * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
         * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
         */
        function _checkRole(bytes32 role) internal view virtual {
            _checkRole(role, _msgSender());
        }
        /**
         * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
         * is missing `role`.
         */
        function _checkRole(bytes32 role, address account) internal view virtual {
            if (!hasRole(role, account)) {
                revert AccessControlUnauthorizedAccount(account, role);
            }
        }
        /**
         * @dev Returns the admin role that controls `role`. See {grantRole} and
         * {revokeRole}.
         *
         * To change a role's admin, use {_setRoleAdmin}.
         */
        function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
            return _roles[role].adminRole;
        }
        /**
         * @dev Grants `role` to `account`.
         *
         * If `account` had not been already granted `role`, emits a {RoleGranted}
         * event.
         *
         * Requirements:
         *
         * - the caller must have ``role``'s admin role.
         *
         * May emit a {RoleGranted} event.
         */
        function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
            _grantRole(role, account);
        }
        /**
         * @dev Revokes `role` from `account`.
         *
         * If `account` had been granted `role`, emits a {RoleRevoked} event.
         *
         * Requirements:
         *
         * - the caller must have ``role``'s admin role.
         *
         * May emit a {RoleRevoked} event.
         */
        function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
            _revokeRole(role, account);
        }
        /**
         * @dev Revokes `role` from the calling account.
         *
         * Roles are often managed via {grantRole} and {revokeRole}: this function's
         * purpose is to provide a mechanism for accounts to lose their privileges
         * if they are compromised (such as when a trusted device is misplaced).
         *
         * If the calling account had been revoked `role`, emits a {RoleRevoked}
         * event.
         *
         * Requirements:
         *
         * - the caller must be `callerConfirmation`.
         *
         * May emit a {RoleRevoked} event.
         */
        function renounceRole(bytes32 role, address callerConfirmation) public virtual {
            if (callerConfirmation != _msgSender()) {
                revert AccessControlBadConfirmation();
            }
            _revokeRole(role, callerConfirmation);
        }
        /**
         * @dev Sets `adminRole` as ``role``'s admin role.
         *
         * Emits a {RoleAdminChanged} event.
         */
        function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
            bytes32 previousAdminRole = getRoleAdmin(role);
            _roles[role].adminRole = adminRole;
            emit RoleAdminChanged(role, previousAdminRole, adminRole);
        }
        /**
         * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
         *
         * Internal function without access restriction.
         *
         * May emit a {RoleGranted} event.
         */
        function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
            if (!hasRole(role, account)) {
                _roles[role].hasRole[account] = true;
                emit RoleGranted(role, account, _msgSender());
                return true;
            } else {
                return false;
            }
        }
        /**
         * @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
         *
         * Internal function without access restriction.
         *
         * May emit a {RoleRevoked} event.
         */
        function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
            if (hasRole(role, account)) {
                _roles[role].hasRole[account] = false;
                emit RoleRevoked(role, account, _msgSender());
                return true;
            } else {
                return false;
            }
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol)
    // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
    pragma solidity ^0.8.20;
    /**
     * @dev Library for managing
     * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
     * types.
     *
     * Sets have the following properties:
     *
     * - Elements are added, removed, and checked for existence in constant time
     * (O(1)).
     * - Elements are enumerated in O(n). No guarantees are made on the ordering.
     *
     * ```solidity
     * contract Example {
     *     // Add the library methods
     *     using EnumerableSet for EnumerableSet.AddressSet;
     *
     *     // Declare a set state variable
     *     EnumerableSet.AddressSet private mySet;
     * }
     * ```
     *
     * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
     * and `uint256` (`UintSet`) are supported.
     *
     * [WARNING]
     * ====
     * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
     * unusable.
     * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
     *
     * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
     * array of EnumerableSet.
     * ====
     */
    library EnumerableSet {
        // To implement this library for multiple types with as little code
        // repetition as possible, we write it in terms of a generic Set type with
        // bytes32 values.
        // The Set implementation uses private functions, and user-facing
        // implementations (such as AddressSet) are just wrappers around the
        // underlying Set.
        // This means that we can only create new EnumerableSets for types that fit
        // in bytes32.
        struct Set {
            // Storage of set values
            bytes32[] _values;
            // Position is the index of the value in the `values` array plus 1.
            // Position 0 is used to mean a value is not in the set.
            mapping(bytes32 value => uint256) _positions;
        }
        /**
         * @dev Add a value to a set. O(1).
         *
         * Returns true if the value was added to the set, that is if it was not
         * already present.
         */
        function _add(Set storage set, bytes32 value) private returns (bool) {
            if (!_contains(set, value)) {
                set._values.push(value);
                // The value is stored at length-1, but we add 1 to all indexes
                // and use 0 as a sentinel value
                set._positions[value] = set._values.length;
                return true;
            } else {
                return false;
            }
        }
        /**
         * @dev Removes a value from a set. O(1).
         *
         * Returns true if the value was removed from the set, that is if it was
         * present.
         */
        function _remove(Set storage set, bytes32 value) private returns (bool) {
            // We cache the value's position to prevent multiple reads from the same storage slot
            uint256 position = set._positions[value];
            if (position != 0) {
                // Equivalent to contains(set, value)
                // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
                // the array, and then remove the last element (sometimes called as 'swap and pop').
                // This modifies the order of the array, as noted in {at}.
                uint256 valueIndex = position - 1;
                uint256 lastIndex = set._values.length - 1;
                if (valueIndex != lastIndex) {
                    bytes32 lastValue = set._values[lastIndex];
                    // Move the lastValue to the index where the value to delete is
                    set._values[valueIndex] = lastValue;
                    // Update the tracked position of the lastValue (that was just moved)
                    set._positions[lastValue] = position;
                }
                // Delete the slot where the moved value was stored
                set._values.pop();
                // Delete the tracked position for the deleted slot
                delete set._positions[value];
                return true;
            } else {
                return false;
            }
        }
        /**
         * @dev Returns true if the value is in the set. O(1).
         */
        function _contains(Set storage set, bytes32 value) private view returns (bool) {
            return set._positions[value] != 0;
        }
        /**
         * @dev Returns the number of values on the set. O(1).
         */
        function _length(Set storage set) private view returns (uint256) {
            return set._values.length;
        }
        /**
         * @dev Returns the value stored at position `index` in the set. O(1).
         *
         * Note that there are no guarantees on the ordering of values inside the
         * array, and it may change when more values are added or removed.
         *
         * Requirements:
         *
         * - `index` must be strictly less than {length}.
         */
        function _at(Set storage set, uint256 index) private view returns (bytes32) {
            return set._values[index];
        }
        /**
         * @dev Return the entire set in an array
         *
         * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
         * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
         * this function has an unbounded cost, and using it as part of a state-changing function may render the function
         * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
         */
        function _values(Set storage set) private view returns (bytes32[] memory) {
            return set._values;
        }
        // Bytes32Set
        struct Bytes32Set {
            Set _inner;
        }
        /**
         * @dev Add a value to a set. O(1).
         *
         * Returns true if the value was added to the set, that is if it was not
         * already present.
         */
        function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
            return _add(set._inner, value);
        }
        /**
         * @dev Removes a value from a set. O(1).
         *
         * Returns true if the value was removed from the set, that is if it was
         * present.
         */
        function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
            return _remove(set._inner, value);
        }
        /**
         * @dev Returns true if the value is in the set. O(1).
         */
        function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
            return _contains(set._inner, value);
        }
        /**
         * @dev Returns the number of values in the set. O(1).
         */
        function length(Bytes32Set storage set) internal view returns (uint256) {
            return _length(set._inner);
        }
        /**
         * @dev Returns the value stored at position `index` in the set. O(1).
         *
         * Note that there are no guarantees on the ordering of values inside the
         * array, and it may change when more values are added or removed.
         *
         * Requirements:
         *
         * - `index` must be strictly less than {length}.
         */
        function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
            return _at(set._inner, index);
        }
        /**
         * @dev Return the entire set in an array
         *
         * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
         * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
         * this function has an unbounded cost, and using it as part of a state-changing function may render the function
         * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
         */
        function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
            bytes32[] memory store = _values(set._inner);
            bytes32[] memory result;
            /// @solidity memory-safe-assembly
            assembly {
                result := store
            }
            return result;
        }
        // AddressSet
        struct AddressSet {
            Set _inner;
        }
        /**
         * @dev Add a value to a set. O(1).
         *
         * Returns true if the value was added to the set, that is if it was not
         * already present.
         */
        function add(AddressSet storage set, address value) internal returns (bool) {
            return _add(set._inner, bytes32(uint256(uint160(value))));
        }
        /**
         * @dev Removes a value from a set. O(1).
         *
         * Returns true if the value was removed from the set, that is if it was
         * present.
         */
        function remove(AddressSet storage set, address value) internal returns (bool) {
            return _remove(set._inner, bytes32(uint256(uint160(value))));
        }
        /**
         * @dev Returns true if the value is in the set. O(1).
         */
        function contains(AddressSet storage set, address value) internal view returns (bool) {
            return _contains(set._inner, bytes32(uint256(uint160(value))));
        }
        /**
         * @dev Returns the number of values in the set. O(1).
         */
        function length(AddressSet storage set) internal view returns (uint256) {
            return _length(set._inner);
        }
        /**
         * @dev Returns the value stored at position `index` in the set. O(1).
         *
         * Note that there are no guarantees on the ordering of values inside the
         * array, and it may change when more values are added or removed.
         *
         * Requirements:
         *
         * - `index` must be strictly less than {length}.
         */
        function at(AddressSet storage set, uint256 index) internal view returns (address) {
            return address(uint160(uint256(_at(set._inner, index))));
        }
        /**
         * @dev Return the entire set in an array
         *
         * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
         * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
         * this function has an unbounded cost, and using it as part of a state-changing function may render the function
         * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
         */
        function values(AddressSet storage set) internal view returns (address[] memory) {
            bytes32[] memory store = _values(set._inner);
            address[] memory result;
            /// @solidity memory-safe-assembly
            assembly {
                result := store
            }
            return result;
        }
        // UintSet
        struct UintSet {
            Set _inner;
        }
        /**
         * @dev Add a value to a set. O(1).
         *
         * Returns true if the value was added to the set, that is if it was not
         * already present.
         */
        function add(UintSet storage set, uint256 value) internal returns (bool) {
            return _add(set._inner, bytes32(value));
        }
        /**
         * @dev Removes a value from a set. O(1).
         *
         * Returns true if the value was removed from the set, that is if it was
         * present.
         */
        function remove(UintSet storage set, uint256 value) internal returns (bool) {
            return _remove(set._inner, bytes32(value));
        }
        /**
         * @dev Returns true if the value is in the set. O(1).
         */
        function contains(UintSet storage set, uint256 value) internal view returns (bool) {
            return _contains(set._inner, bytes32(value));
        }
        /**
         * @dev Returns the number of values in the set. O(1).
         */
        function length(UintSet storage set) internal view returns (uint256) {
            return _length(set._inner);
        }
        /**
         * @dev Returns the value stored at position `index` in the set. O(1).
         *
         * Note that there are no guarantees on the ordering of values inside the
         * array, and it may change when more values are added or removed.
         *
         * Requirements:
         *
         * - `index` must be strictly less than {length}.
         */
        function at(UintSet storage set, uint256 index) internal view returns (uint256) {
            return uint256(_at(set._inner, index));
        }
        /**
         * @dev Return the entire set in an array
         *
         * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
         * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
         * this function has an unbounded cost, and using it as part of a state-changing function may render the function
         * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
         */
        function values(UintSet storage set) internal view returns (uint256[] memory) {
            bytes32[] memory store = _values(set._inner);
            uint256[] memory result;
            /// @solidity memory-safe-assembly
            assembly {
                result := store
            }
            return result;
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (access/IAccessControl.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev External interface of AccessControl declared to support ERC-165 detection.
     */
    interface IAccessControl {
        /**
         * @dev The `account` is missing a role.
         */
        error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
        /**
         * @dev The caller of a function is not the expected one.
         *
         * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
         */
        error AccessControlBadConfirmation();
        /**
         * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
         *
         * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
         * {RoleAdminChanged} not being emitted signaling this.
         */
        event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
        /**
         * @dev Emitted when `account` is granted `role`.
         *
         * `sender` is the account that originated the contract call, an admin role
         * bearer except when using {AccessControl-_setupRole}.
         */
        event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
        /**
         * @dev Emitted when `account` is revoked `role`.
         *
         * `sender` is the account that originated the contract call:
         *   - if using `revokeRole`, it is the admin role bearer
         *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
         */
        event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
        /**
         * @dev Returns `true` if `account` has been granted `role`.
         */
        function hasRole(bytes32 role, address account) external view returns (bool);
        /**
         * @dev Returns the admin role that controls `role`. See {grantRole} and
         * {revokeRole}.
         *
         * To change a role's admin, use {AccessControl-_setRoleAdmin}.
         */
        function getRoleAdmin(bytes32 role) external view returns (bytes32);
        /**
         * @dev Grants `role` to `account`.
         *
         * If `account` had not been already granted `role`, emits a {RoleGranted}
         * event.
         *
         * Requirements:
         *
         * - the caller must have ``role``'s admin role.
         */
        function grantRole(bytes32 role, address account) external;
        /**
         * @dev Revokes `role` from `account`.
         *
         * If `account` had been granted `role`, emits a {RoleRevoked} event.
         *
         * Requirements:
         *
         * - the caller must have ``role``'s admin role.
         */
        function revokeRole(bytes32 role, address account) external;
        /**
         * @dev Revokes `role` from the calling account.
         *
         * Roles are often managed via {grantRole} and {revokeRole}: this function's
         * purpose is to provide a mechanism for accounts to lose their privileges
         * if they are compromised (such as when a trusted device is misplaced).
         *
         * If the calling account had been granted `role`, emits a {RoleRevoked}
         * event.
         *
         * Requirements:
         *
         * - the caller must be `callerConfirmation`.
         */
        function renounceRole(bytes32 role, address callerConfirmation) external;
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Provides information about the current execution context, including the
     * sender of the transaction and its data. While these are generally available
     * via msg.sender and msg.data, they should not be accessed in such a direct
     * manner, since when dealing with meta-transactions the account sending and
     * paying for execution may not be the actual sender (as far as an application
     * is concerned).
     *
     * This contract is only required for intermediate, library-like contracts.
     */
    abstract contract Context {
        function _msgSender() internal view virtual returns (address) {
            return msg.sender;
        }
        function _msgData() internal view virtual returns (bytes calldata) {
            return msg.data;
        }
        function _contextSuffixLength() internal view virtual returns (uint256) {
            return 0;
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol)
    pragma solidity ^0.8.20;
    import {IERC165} from "./IERC165.sol";
    /**
     * @dev Implementation of the {IERC165} interface.
     *
     * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
     * for the additional interface id that will be supported. For example:
     *
     * ```solidity
     * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
     *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
     * }
     * ```
     */
    abstract contract ERC165 is IERC165 {
        /**
         * @dev See {IERC165-supportsInterface}.
         */
        function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
            return interfaceId == type(IERC165).interfaceId;
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Interface of the ERC-165 standard, as defined in the
     * https://eips.ethereum.org/EIPS/eip-165[ERC].
     *
     * Implementers can declare support of contract interfaces, which can then be
     * queried by others ({ERC165Checker}).
     *
     * For an implementation, see {ERC165}.
     */
    interface IERC165 {
        /**
         * @dev Returns true if this contract implements the interface defined by
         * `interfaceId`. See the corresponding
         * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
         * to learn more about how these ids are created.
         *
         * This function call must use less than 30 000 gas.
         */
        function supportsInterface(bytes4 interfaceId) external view returns (bool);
    }