// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.0;

import '@mimic-fi/v2-bridge-connector/contracts/interfaces/IHopL2AMM.sol';
import '@mimic-fi/v2-helpers/contracts/utils/EnumerableMap.sol';
import '@mimic-fi/v2-swap-connector/contracts/ISwapConnector.sol';

import './BaseSwapper.sol';
import './interfaces/IL2HopSwapper.sol';

contract L2HopSwapper is IL2HopSwapper, BaseSwapper {
    using FixedPoint for uint256;
    using EnumerableMap for EnumerableMap.AddressToAddressMap;

    // Base gas amount charged to cover gas payment
    uint256 public constant override BASE_GAS = 30e3;

    // List of AMMs per token
    EnumerableMap.AddressToAddressMap private _tokenAmms;

    struct TokenAmm {
        address token;
        address amm;
    }

    /**
     * @dev L2 Hop swapper action config
     */
    struct L2HopSwapperConfig {
        TokenAmm[] tokenAmms;
        SwapperConfig swapperConfig;
    }

    /**
     * @dev Creates a L2 Hop swapper action
     */
    constructor(L2HopSwapperConfig memory config) BaseSwapper(config.swapperConfig) {
        for (uint256 i = 0; i < config.tokenAmms.length; i++) {
            _setTokenAmm(config.tokenAmms[i].token, config.tokenAmms[i].amm);
        }
    }

    /**
     * @dev Tells AMM set for a token
     */
    function getTokenAmm(address token) public view override returns (address amm) {
        (, amm) = _tokenAmms.tryGet(token);
    }

    /**
     * @dev Tells the list of AMMs set for each token
     */
    function getTokenAmms() external view override returns (address[] memory tokens, address[] memory amms) {
        tokens = _tokenAmms.keys();
        amms = _tokenAmms.values();
    }

    /**
     * @dev Sets a list of amms for a list of hTokens
     * @param hTokens List of hToken addresses to be set
     * @param amms List of AMM addresses to be set for each hToken
     */
    function setTokenAmms(address[] memory hTokens, address[] memory amms) external override auth {
        _setTokenAmms(hTokens, amms);
    }

    /**
     * @dev Execution function
     */
    function call(address hToken, uint256 amount, uint256 slippage) external override actionCall(hToken, amount) {
        _validateAmm(hToken);
        _validateSlippage(hToken, slippage);

        address tokenOut = _getApplicableTokenOut(hToken);
        bytes memory data = abi.encode(IHopL2AMM(getTokenAmm(hToken)).exchangeAddress());
        uint256 minAmountOut = amount.mulUp(FixedPoint.ONE - slippage);

        smartVault.swap(
            uint8(ISwapConnector.Source.Hop),
            hToken,
            tokenOut,
            amount,
            ISmartVault.SwapLimit.MinAmountOut,
            minAmountOut,
            data
        );
    }

    /**
     * @dev Tells if a token has an AMM set
     */
    function _isAmmValid(address token) internal view returns (bool) {
        return _tokenAmms.contains(token);
    }

    /**
     * @dev Reverts if there is no Hop AMM set for a given hToken
     */
    function _validateAmm(address hToken) internal view {
        require(_isAmmValid(hToken), 'ACTION_MISSING_HOP_TOKEN_AMM');
    }

    /**
     * @dev Sets a list of AMMs for a list of hTokens
     * @param hTokens List of hToken addresses to be set
     * @param amms List of AMM addresses to be set for each hToken
     */
    function _setTokenAmms(address[] memory hTokens, address[] memory amms) internal {
        require(hTokens.length == amms.length, 'ACTION_TOKENS_AMMS_BAD_INPUT_LEN');
        for (uint256 i = 0; i < hTokens.length; i++) {
            _setTokenAmm(hTokens[i], amms[i]);
        }
    }

    /**
     * @dev Set an AMM for a Hop token
     * @param hToken Address of the hToken to set an AMM for
     * @param amm AMM to be set
     */
    function _setTokenAmm(address hToken, address amm) internal {
        require(hToken != address(0), 'ACTION_HOP_TOKEN_ZERO');
        require(amm == address(0) || hToken == IHopL2AMM(amm).hToken(), 'ACTION_HOP_TOKEN_AMM_MISMATCH');

        amm == address(0) ? _tokenAmms.remove(hToken) : _tokenAmms.set(hToken, amm);
        emit TokenAmmSet(hToken, amm);
    }
}
[{"inputs":[{"components":[{"internalType":"address","name":"recipient","type":"address"},{"components":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"smartVault","type":"address"},{"internalType":"uint8","name":"groupId","type":"uint8"}],"internalType":"struct BaseAction.BaseConfig","name":"baseConfig","type":"tuple"},{"components":[{"internalType":"uint256","name":"gasPriceLimit","type":"uint256"},{"internalType":"uint256","name":"priorityFeeLimit","type":"uint256"},{"internalType":"uint256","name":"txCostLimit","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"bool","name":"permissiveMode","type":"bool"},{"internalType":"address[]","name":"relayers","type":"address[]"}],"internalType":"struct RelayedAction.RelayConfig","name":"relayConfig","type":"tuple"},{"components":[{"internalType":"address[]","name":"signers","type":"address[]"}],"internalType":"struct OracledAction.OracleConfig","name":"oracleConfig","type":"tuple"},{"components":[{"internalType":"uint256","name":"delay","type":"uint256"},{"internalType":"uint256","name":"nextExecutionTimestamp","type":"uint256"}],"internalType":"struct TimeLockedAction.TimeLockConfig","name":"timeLockConfig","type":"tuple"},{"components":[{"internalType":"address[]","name":"tokens","type":"address[]"},{"internalType":"address[]","name":"sources","type":"address[]"},{"internalType":"enum ITokenIndexedAction.TokensAcceptanceType","name":"acceptanceType","type":"uint8"}],"internalType":"struct TokenIndexedAction.TokenIndexConfig","name":"tokenIndexConfig","type":"tuple"},{"components":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold","name":"defaultThreshold","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold","name":"threshold","type":"tuple"}],"internalType":"struct TokenThresholdAction.CustomThreshold[]","name":"customThresholds","type":"tuple[]"}],"internalType":"struct TokenThresholdAction.TokenThresholdConfig","name":"tokenThresholdConfig","type":"tuple"}],"internalType":"struct Action.ActionConfig","name":"actionConfig","type":"tuple"}],"internalType":"struct Withdrawer.WithdrawerConfig","name":"config","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"bytes4","name":"what","type":"bytes4"}],"name":"Authorized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"indexed":false,"internalType":"struct ITokenThresholdAction.Threshold","name":"threshold","type":"tuple"}],"name":"CustomTokenThresholdSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"}],"name":"CustomTokenThresholdUnset","type":"event"},{"anonymous":false,"inputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"indexed":false,"internalType":"struct ITokenThresholdAction.Threshold","name":"threshold","type":"tuple"}],"name":"DefaultTokenThresholdSet","type":"event"},{"anonymous":false,"inputs":[],"name":"DefaultTokenThresholdUnset","type":"event"},{"anonymous":false,"inputs":[],"name":"Executed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint8","name":"groupId","type":"uint8"}],"name":"GroupIdSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"signer","type":"address"}],"name":"OracleSignerAllowed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"signer","type":"address"}],"name":"OracleSignerDisallowed","type":"event"},{"anonymous":false,"inputs":[],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"}],"name":"RecipientSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"gasPriceLimit","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"priorityFeeLimit","type":"uint256"}],"name":"RelayGasLimitsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"}],"name":"RelayGasTokenSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"active","type":"bool"}],"name":"RelayPermissiveModeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"txCostLimit","type":"uint256"}],"name":"RelayTxCostLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"relayer","type":"address"}],"name":"RelayerAllowed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"relayer","type":"address"}],"name":"RelayerDisallowed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"delay","type":"uint256"}],"name":"TimeLockDelaySet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"expiration","type":"uint256"}],"name":"TimeLockExpirationSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"source","type":"address"}],"name":"TokenIndexSourceAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"source","type":"address"}],"name":"TokenIndexSourceRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"}],"name":"TokensAcceptanceAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"}],"name":"TokensAcceptanceRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"enum ITokenIndexedAction.TokensAcceptanceType","name":"acceptanceType","type":"uint8"}],"name":"TokensAcceptanceTypeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"bytes4","name":"what","type":"bytes4"}],"name":"Unauthorized","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpaused","type":"event"},{"inputs":[],"name":"ANY_ADDRESS","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"BASE_GAS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"bytes4","name":"what","type":"bytes4"}],"name":"authorize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"call","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getActionBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getCustomTokenThreshold","outputs":[{"internalType":"bool","name":"exists","type":"bool"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold","name":"threshold","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCustomTokenThresholds","outputs":[{"internalType":"address[]","name":"tokens","type":"address[]"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold[]","name":"thresholds","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDefaultTokenThreshold","outputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"base","type":"address"},{"internalType":"address","name":"quote","type":"address"},{"internalType":"uint256","name":"rate","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"internalType":"struct IOracledAction.FeedData[]","name":"feeds","type":"tuple[]"}],"name":"getFeedsDigest","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getGroupId","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOracleSigners","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRelayGasLimits","outputs":[{"internalType":"uint256","name":"gasPriceLimit","type":"uint256"},{"internalType":"uint256","name":"priorityFeeLimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRelayGasToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRelayTxCostLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRelayers","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getSmartVaultBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTimeLock","outputs":[{"internalType":"uint256","name":"delay","type":"uint256"},{"internalType":"uint256","name":"expiresAt","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTokensAcceptanceList","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTokensAcceptanceType","outputs":[{"internalType":"enum ITokenIndexedAction.TokensAcceptanceType","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTokensIndexSources","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getTotalBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"bytes4","name":"what","type":"bytes4"}],"name":"isAuthorized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"signer","type":"address"}],"name":"isOracleSigner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isRelayPermissiveModeActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"relayer","type":"address"}],"name":"isRelayer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isTimeLockExpired","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"isTokenAllowed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokens","type":"address[]"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold[]","name":"thresholds","type":"tuple[]"}],"name":"setCustomTokenThresholds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"min","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"}],"internalType":"struct ITokenThresholdAction.Threshold","name":"threshold","type":"tuple"}],"name":"setDefaultTokenThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"groupId","type":"uint8"}],"name":"setGroupId","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"toAdd","type":"address[]"},{"internalType":"address[]","name":"toRemove","type":"address[]"}],"name":"setOracleSigners","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"}],"name":"setRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"gasPriceLimit","type":"uint256"},{"internalType":"uint256","name":"priorityFeeLimit","type":"uint256"}],"name":"setRelayGasLimits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"setRelayGasToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"active","type":"bool"}],"name":"setRelayPermissiveMode","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"txCostLimit","type":"uint256"}],"name":"setRelayTxCostLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"relayersToAdd","type":"address[]"},{"internalType":"address[]","name":"relayersToRemove","type":"address[]"}],"name":"setRelayers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"delay","type":"uint256"}],"name":"setTimeLockDelay","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"setTimeLockExpiration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"toAdd","type":"address[]"},{"internalType":"address[]","name":"toRemove","type":"address[]"}],"name":"setTokensAcceptanceList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum ITokenIndexedAction.TokensAcceptanceType","name":"acceptanceType","type":"uint8"}],"name":"setTokensAcceptanceType","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"toAdd","type":"address[]"},{"internalType":"address[]","name":"toRemove","type":"address[]"}],"name":"setTokensIndexSources","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"smartVault","outputs":[{"internalType":"contract ISmartVault","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferToSmartVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"bytes4","name":"what","type":"bytes4"}],"name":"unauthorize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokens","type":"address[]"}],"name":"unsetCustomTokenThresholds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unsetDefaultTokenThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]