// File: src/TSAggregatorStargate.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {SafeTransferLib} from "../lib/SafeTransferLib.sol";
import {TSAggregator} from "./TSAggregator.sol";
import {IERC20} from "./interfaces/IERC20.sol";
import {IOracle} from "./interfaces/IOracle.sol";
import {IStargateRouter} from "./interfaces/IStargateRouter.sol";
import {IThorchainRouter} from "./interfaces/IThorchainRouter.sol";
import {IUniswapRouterV2} from "./interfaces/IUniswapRouterV2.sol";

contract TSAggregatorStargate is TSAggregator {
    error NotAConfiguredToken();

    using SafeTransferLib for address;

    struct TokenConfig {
        uint256 chainId;
        address token;
        address router;
        address[] path;
    }

    IStargateRouter public stargate;
    IUniswapRouterV2 public router;
    IERC20 public bridgeToken;
    IOracle public ethOracle;
    uint256 public slippage = 100;
    uint256 public sourcePoolId = 1;
    mapping(uint256 => uint256) public chainTargetPoolId;
    mapping(uint256 => address) public chainTargetContract;
    mapping(uint256 => TokenConfig) public tokens;

    event SetTokenConfig(uint256 indexed id, uint256 chainId, address token, address router, address[] path);
    event SwapIn(address from, address token, uint256 amount, uint256 out, uint256 fee, address vault, string memo);
    event SwapOut(address to, address token, uint256 amount, uint256 fee);

    constructor(address _stargate, address _router, address _bridgeToken, address _ethOracle)
        TSAggregator(address(0))
    {
        stargate = IStargateRouter(_stargate);
        router = IUniswapRouterV2(_router);
        bridgeToken = IERC20(_bridgeToken);
        ethOracle = IOracle(_ethOracle);
        chainTargetPoolId[102] = 2; // BNB (USDT)
        chainTargetPoolId[106] = 1; // Avalanche
        chainTargetPoolId[109] = 1; // Polygon
        chainTargetPoolId[110] = 1; // Arbitrum
        chainTargetPoolId[111] = 1; // Optimism
        chainTargetPoolId[112] = 1; // Fantom
    }

    function getTokensPath(uint256 id) external view returns (address[] memory) {
        return tokens[id].path;
    }

    function setSlippage(uint256 _slippage) external isOwner {
        slippage = _slippage;
    }

    function setChainTargetContract(uint256 chainId, address targetContract) external isOwner {
        chainTargetContract[chainId] = targetContract;
    }

    function setChainTargetPoolId(uint256 chainId, uint256 poolId) external isOwner {
        chainTargetPoolId[chainId] = poolId;
    }

    function setTokenConfig(uint256 id, uint256 chainId, address token, address router, address[] calldata path) external isOwner {
        tokens[id] = TokenConfig({chainId: chainId, token: token, router: router, path: path});
        emit SetTokenConfig(id, chainId, token, router, path);
    }

    function sgReceive(uint16, bytes memory, uint256, address bridgeToken, uint256 bridgeAmount, bytes memory payload)
        external
    {
        require(msg.sender == address(stargate), "!stargate");
        (address tcRouter, address vault, string memory memo, address from, uint256 deadline) =
            abi.decode(payload, (address, address, string, address, uint256));
        uint256 price = uint256(ethOracle.latestAnswer()) * 1e18 / ethOracle.decimals();
        uint256 minAmtOut = _slip(bridgeAmount) * (10 ** IERC20(bridgeToken).decimals()) / price;
        IERC20(bridgeToken).approve(address(router), bridgeAmount);
        address[] memory path = new address[](2);
        path[0] = bridgeToken;
        path[1] = router.WETH();
        try router.swapExactTokensForETH(bridgeAmount, minAmtOut, path, address(this), deadline) {
            uint256 out = address(this).balance;
            uint256 outMinusFee = skimFee(out);
            IThorchainRouter(tcRouter).depositWithExpiry{value: outMinusFee}(
                payable(vault), address(0), outMinusFee, memo, deadline
            );
            emit SwapIn(msg.sender, bridgeToken, bridgeAmount, out, out - outMinusFee, vault, memo);
        } catch {
            IERC20(bridgeToken).transfer(from, bridgeAmount);
        }
    }

    function swapOut(address token, address to, uint256 amountOutMin) public payable nonReentrant {
        uint256 tokenId = amountOutMin % 100000 / 100;
        amountOutMin = _parseAmountOutMin(amountOutMin);
        IStargateRouter.lzTxObj memory txObj = IStargateRouter.lzTxObj(500000, 0, "0x");
        TokenConfig memory tokenConfig = tokens[tokenId];
        if (tokenConfig.token == address(0)) revert NotAConfiguredToken();
        bytes memory data = abi.encode(tokenConfig.token, tokenConfig.router, tokenConfig.path, to, amountOutMin);

        address targetContract = chainTargetContract[tokenConfig.chainId];
        uint256 amount = skimFee(msg.value);
        (uint256 fee,) = stargate.quoteLayerZeroFee(
            uint16(tokenConfig.chainId), uint8(1), abi.encodePacked(targetContract), data, txObj
        );

        {
            uint256 price = uint256(ethOracle.latestAnswer()) * 1e18 / ethOracle.decimals();
            uint256 minAmtOut = _slip(amount - fee) * (10 ** bridgeToken.decimals()) / price;
            address[] memory path = new address[](2);
            path[0] = address(router.WETH());
            path[1] = address(bridgeToken);
            router.swapExactETHForTokens{value: amount - fee}(minAmtOut, path, address(this), type(uint256).max);
        }

        uint256 tokenAmount = bridgeToken.balanceOf(address(this));
        bridgeToken.approve(address(stargate), tokenAmount);
        stargate.swap{value: fee}(
            uint16(tokenConfig.chainId),
            sourcePoolId,
            chainTargetPoolId[tokenConfig.chainId],
            payable(to),
            tokenAmount,
            _slip(tokenAmount),
            txObj,
            abi.encodePacked(targetContract),
            data
        );

        emit SwapOut(to, token, msg.value, msg.value - amount);
    }

    function _slip(uint256 amount) internal view returns (uint256) {
        return amount * (10000 - slippage) / 10000;
    }
}

// File: lib/SafeTransferLib.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
library SafeTransferLib {
    /*///////////////////////////////////////////////////////////////
                            ETH OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function safeTransferETH(address to, uint256 amount) internal {
        bool callStatus;

        assembly {
            // Transfer the ETH and store if it succeeded or not.
            callStatus := call(gas(), to, amount, 0, 0, 0, 0)
        }

        require(callStatus, "ETH_TRANSFER_FAILED");
    }

    /*///////////////////////////////////////////////////////////////
                           ERC20 OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
            mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "from" argument.
            mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 100 because the calldata length is 4 + 32 * 3.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0)
        }

        require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FROM_FAILED");
    }

    function safeTransfer(
        address token,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
            mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 68 because the calldata length is 4 + 32 * 2.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
        }

        require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FAILED");
    }

    function safeApprove(
        address token,
        address to,
        uint256 amount
    ) internal {
        bool callStatus;

        assembly {
            // Get a pointer to some free memory.
            let freeMemoryPointer := mload(0x40)

            // Write the abi-encoded calldata to memory piece by piece:
            mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
            mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
            mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.

            // Call the token and store if it succeeded or not.
            // We use 68 because the calldata length is 4 + 32 * 2.
            callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
        }

        require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED");
    }

    /*///////////////////////////////////////////////////////////////
                         INTERNAL HELPER LOGIC
    //////////////////////////////////////////////////////////////*/

    function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) {
        assembly {
            // Get how many bytes the call returned.
            let returnDataSize := returndatasize()

            // If the call reverted:
            if iszero(callStatus) {
                // Copy the revert message into memory.
                returndatacopy(0, 0, returnDataSize)

                // Revert with the same message.
                revert(0, returnDataSize)
            }

            switch returnDataSize
            case 32 {
                // Copy the return data into memory.
                returndatacopy(0, 0, returnDataSize)

                // Set success to whether it returned true.
                success := iszero(iszero(mload(0)))
            }
            case 0 {
                // There was no return data.
                success := 1
            }
            default {
                // It returned some malformed input.
                success := 0
            }
        }
    }
}

// File: src/TSAggregator.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import { SafeTransferLib } from "../lib/SafeTransferLib.sol";
import { ReentrancyGuard } from "../lib/ReentrancyGuard.sol";
import { Owners } from "./Owners.sol";
import { TSAggregatorTokenTransferProxy } from './TSAggregatorTokenTransferProxy.sol';

abstract contract TSAggregator is Owners, ReentrancyGuard {
    using SafeTransferLib for address;

    event FeeSet(uint256 fee, address feeRecipient);

    uint256 public fee;
    address public feeRecipient;
    TSAggregatorTokenTransferProxy public tokenTransferProxy;

    constructor(address _tokenTransferProxy) {
        _setOwner(msg.sender, true);
        tokenTransferProxy = TSAggregatorTokenTransferProxy(_tokenTransferProxy);
    }

    // Needed for the swap router to be able to send back ETH
    receive() external payable {}

    function setFee(uint256 _fee, address _feeRecipient) external isOwner {
        require(_fee <= 1000, "fee can not be more than 10%");
        fee = _fee;
        feeRecipient = _feeRecipient;
        emit FeeSet(_fee, _feeRecipient);
    }

    function skimFee(uint256 amount) internal returns (uint256) {
        uint256 amountFee = getFee(amount);
        if (amountFee > 0) {
            feeRecipient.safeTransferETH(amountFee);
            amount -= amountFee;
        }
        return amount;
    }

    function getFee(uint256 amount) internal view returns (uint256) {
        if (fee != 0 && feeRecipient != address(0)) {
            return (amount * fee) / 10000;
        }
        return 0;
    }

    // Parse amountOutMin treating the last 2 digits as an exponent
    // So 1504 = 150000. This allows for compressed memos on chains
    // with limited space like Bitcoin
    function _parseAmountOutMin(uint256 amount) internal pure returns (uint256) {
      return amount / 100 * (10 ** (amount % 100));
    }
}

// File: src/interfaces/IERC20.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IERC20 {
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

// File: src/interfaces/IOracle.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IOracle {
    function latestAnswer() external view returns (int256);
    function decimals() external view returns (uint8);
}

// File: src/interfaces/IStargateRouter.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IStargateRouter {
    struct lzTxObj {
        uint256 dstGasForCall;
        uint256 dstNativeAmount;
        bytes dstNativeAddr;
    }

    function quoteLayerZeroFee(
        uint16 _dstChainId,
        uint8 _functionType,
        bytes calldata _toAddress,
        bytes calldata _transferAndCallPayload,
        lzTxObj memory _lzTxParams
    ) external view returns (uint256, uint256);
    function swap(
        uint16 _dstChainId,
        uint256 _srcPoolId,
        uint256 _dstPoolId,
        address payable _refundAddress,
        uint256 _amountLD,
        uint256 _minAmountLD,
        lzTxObj memory _lzTxParams,
        bytes calldata _to,
        bytes calldata _payload
    ) external payable;
}

// File: src/interfaces/IThorchainRouter.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IThorchainRouter {
    function depositWithExpiry(
        address payable vault,
        address asset,
        uint amount,
        string memory memo,
        uint expiration
    ) external payable;
}

// File: src/interfaces/IUniswapRouterV2.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IUniswapRouterV2 {
    function WETH() external view returns (address);
    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
    function swapExactETHForTokens(
        uint amountOutMin,
        address[] calldata path,
        address to, uint deadline
    ) external payable;
}

// File: lib/ReentrancyGuard.sol



// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Gas optimized reentrancy protection for smart contracts.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol)
abstract contract ReentrancyGuard {
    uint256 private locked = 1;

    modifier nonReentrant() {
        require(locked == 1, "REENTRANCY");

        locked = 2;

        _;

        locked = 1;
    }
}

// File: src/Owners.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

abstract contract Owners {
    event OwnerSet(address indexed owner, bool active);

    mapping(address => bool) public owners;

    modifier isOwner() {
        require(owners[msg.sender], "Unauthorized");
        _;
    }

    function _setOwner(address owner, bool active) internal virtual {
      owners[owner] = active;
      emit OwnerSet(owner, active);
    }

    function setOwner(address owner, bool active) external virtual isOwner {
      _setOwner(owner, active);
    }
}

// File: src/TSAggregatorTokenTransferProxy.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import { SafeTransferLib } from "../lib/SafeTransferLib.sol";
import { Owners } from "./Owners.sol";

contract TSAggregatorTokenTransferProxy is Owners {
    using SafeTransferLib for address;

    constructor() {
        _setOwner(msg.sender, true);
    }

    function transferTokens(address token, address from, address to, uint256 amount) external isOwner {
        require(from == tx.origin || _isContract(from), "Invalid from address");
        token.safeTransferFrom(from, to, amount);
    }

    function _isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.
        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}