// 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";

// 101 1 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 (WETH) Ethereum
// 102 2 0x4Fabb145d64652a948d72533023f6E7A623C7C53 (BUSD) BNB (USDT)
// 106 1 0xfe9A29aB92522D14Fc65880d817214261D8479AE (SNOW) Avalanche
// 109 1 0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0 (MATIC) Polygon
// 110 1 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5 (cETH) Arbitrum
// 111 1 0x030bA81f1c18d280636F32af80b9AAd02Cf0854e (aETH) Optimism
// 112 1 0x4E15361FD6b4BB609Fa63C81A2be19d873717870 (FTM) Fantom

interface ISwapAdapter {
    function swap(address input, address output, uint256 amount, uint256 amountOutMin, address to, bytes calldata data)
        external;
}

contract TSAggregatorStargate is TSAggregator {
    error UnconfiguredChain();
    error UnconfiguredToken();
    error SwapCallReverted();

    using SafeTransferLib for address;

    struct ChainConfig {
        uint256 chainId;
        uint256 poolId;
        address target;
    }

    struct TokenConfig {
        address token;
        address target;
        bytes data;
    }

    uint256 private constant ACTION_SWAP = 1;
    uint256 private constant ACTION_DEPOSIT = 2;
    uint256 public constant sourcePoolId = 1;
    IStargateRouter public stargate;
    IERC20 public bridgeToken;
    IOracle public ethOracle;
    IUniswapRouterV2 public router;
    uint256 public slippage = 100;
    mapping(address => ChainConfig) public chainConfigs;
    mapping(uint256 => TokenConfig) public tokenConfigs;

    event SetChainConfig(address indexed chainToken, uint256 chainId, uint256 poolId, address target);
    event SetTokenConfig(uint256 indexed id, address token, address target, bytes data);
    event SwapTo(address to, address token, uint256 amount, uint256 amountToken, uint256 fee);
    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);
        bridgeToken = IERC20(_bridgeToken);
        ethOracle = IOracle(_ethOracle);
        router = IUniswapRouterV2(_router);
    }

    function setEthOracle(address _ethOracle) external isOwner {
        ethOracle = IOracle(_ethOracle);
    }

    function setRouter(address _router) external isOwner {
        router = IUniswapRouterV2(_router);
    }

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

    function setChainConfig(address chainToken, uint256 chainId, uint256 poolId, address target) external isOwner {
        chainConfigs[chainToken] = ChainConfig({chainId: chainId, poolId: poolId, target: target});
        emit SetChainConfig(chainToken, chainId, poolId, target);
    }

    function setTokenConfig(uint256 id, address token, address target, bytes calldata data) external isOwner {
        tokenConfigs[id] = TokenConfig({token: token, target: target, data: data});
        emit SetTokenConfig(id, token, target, data);
    }

    // Funds coming from an other chain, we either swap to ETH and deposit into TC,
    // or swap to final target
    function sgReceive(uint16 chainId, bytes memory, uint256, address bridgeToken, uint256 bridgeAmount, bytes memory payload)
        external
    {
        require(msg.sender == address(stargate), "!stargate");
        (uint256 action, bytes memory innerPayload) = abi.decode(payload, (uint256, bytes));
        if (action == ACTION_DEPOSIT) {
            (address tcRouter, address vault, string memory memo, address from, uint256 deadline) =
                abi.decode(innerPayload, (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);
            }
        } else if (action == ACTION_SWAP) {
            (uint256 tokenId, address to, uint256 amountOutMin) =
                abi.decode(innerPayload, (uint256, address, uint256));
            TokenConfig memory tokenConfig = tokenConfigs[tokenId];
            if (tokenConfig.target == address(0)) revert UnconfiguredToken();
            uint256 amountFee = getFee(bridgeAmount);
            if (amountFee > 0) IERC20(bridgeToken).transfer(feeRecipient, amountFee);
            IERC20(bridgeToken).approve(tokenConfig.target, bridgeAmount - amountFee);
            ISwapAdapter(tokenConfig.target).swap(
                bridgeToken, tokenConfig.token, bridgeAmount - amountFee, amountOutMin, to, tokenConfig.data
            );
        }
    }

    // Takes ETH from a TC swap and sends it to a destination chain for the final leg
    // `token` is not needed (we want to avoid whitelisting) so we reuse it as "target chain"
    // `amountOutMin` is still it's normal self, but the 3 least significant digits
    // are used to specify the "tokenId" on the desitination chain (selects a "TokenConfig")
    function swapOut(address token, address to, uint256 amountOutMin) public payable nonReentrant {
        uint256 tokenId = amountOutMin % 100000 / 100;
        amountOutMin = _parseAmountOutMin(amountOutMin);
        ChainConfig memory chainConfig = chainConfigs[token];
        if (chainConfig.target == address(0)) revert UnconfiguredChain();
        uint256 amount = skimFee(msg.value);

        bytes memory data = abi.encode(ACTION_SWAP, abi.encode(tokenId, to, amountOutMin));
        IStargateRouter.lzTxObj memory txObj = IStargateRouter.lzTxObj(500000, 0, "0x");
        (uint256 fee,) = stargate.quoteLayerZeroFee(
            uint16(chainConfig.chainId), uint8(chainConfig.poolId),
            abi.encodePacked(chainConfig.target), 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);
        }

        stargateSwap(chainConfig.chainId, sourcePoolId, chainConfig.poolId, chainConfig.target, data, to, fee);
    }

    // Takes any token on current chain, swaps to bridge token and initiates a TC deposit
    function swapAndDeposit(
        uint256 targetChainId,
        uint256 targetPoolId,
        address targetContract,
        address token,
        address target,
        bytes calldata data,
        uint256 amount,
        bytes calldata payload
    ) external payable nonReentrant {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        IERC20(token).approve(target, amount);
        {
            (bool success,) = target.call(data);
            if (!success) revert SwapCallReverted();
        }
        // payload = address tcRouter, address vault, string calldata memo, uint256 deadline
        bytes memory data = abi.encode(ACTION_DEPOSIT, payload);
        stargateSwap(targetChainId, sourcePoolId, targetPoolId, targetContract, data, msg.sender, 0);
    }

    // Takes any token on current chain, swaps to bridge token and initiates a swap on the target chain
    function swapAndSwap(
        uint256 targetChainId,
        uint256 targetPoolId,
        address targetContract,
        address token,
        address target,
        bytes calldata data,
        uint256 amount,
        uint256 tokenId,
        uint256 amountOutMin
    ) external payable nonReentrant {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        IERC20(token).approve(target, amount);
        {
            (bool success,) = target.call(data);
            if (!success) revert SwapCallReverted();
        }
        bytes memory data = abi.encode(ACTION_SWAP, abi.encode(tokenId, msg.sender, amountOutMin));
        stargateSwap(targetChainId, sourcePoolId, targetPoolId, targetContract, data, msg.sender, 0);
    }

    function stargateSwap(uint256 targetChainId, uint256 sourcePoolId, uint256 targetPoolId, address targetContract, bytes memory data, address to, uint256 fee) internal {
        uint256 tokenAmount = bridgeToken.balanceOf(address(this));
        bridgeToken.approve(address(stargate), tokenAmount);
        IStargateRouter.lzTxObj memory txObj = IStargateRouter.lzTxObj(500000, 0, "0x");
        if (fee == 0) {
            (fee,) = stargate.quoteLayerZeroFee(
                uint16(targetChainId), uint8(targetPoolId),
                abi.encodePacked(targetContract), data, txObj
            );
        }
        stargate.swap{value: fee}(
            uint16(targetChainId),
            sourcePoolId,
            targetPoolId,
            payable(to),
            tokenAmount,
            _slip(tokenAmount),
            txObj,
            abi.encodePacked(targetContract),
            data
        );
        msg.sender.call{value: address(this).balance}("");
    }

    function getFee(uint256 targetChainId, uint256 targetPoolId) external view returns (uint256) {
        IStargateRouter.lzTxObj memory txObj = IStargateRouter.lzTxObj(500000, 0, "0x");
        bytes memory data = abi.encode(address(0), address(0), "=:ETH.ETH:123456", address(0), 123456);
        (uint256 fee,) = stargate.quoteLayerZeroFee(uint16(targetChainId), uint8(targetPoolId), abi.encodePacked(address(0)), data, txObj);
        return fee;
    }

    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;
    }
}