// File: src/contracts/TryLSDGateway.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";

import {ICurvePool1} from "../interfaces/ICurvePool.sol";
import {ICurvePool2} from "../interfaces/ICurvePool.sol";

import {IsfrxETH} from "../interfaces/IsfrxETH.sol";
import {IWstETH} from "../interfaces/IWstETH.sol";

contract TryLSDGateway {
    /*//////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    // Event to be emitted when a user deposits through the Gateway
    event Deposit(address indexed sender, address indexed owner, uint256 ethAmount, uint256 shares);

    event Withdraw(
        address indexed sender, address indexed receiver, address indexed owner, uint256 ethAmount, uint256 shares
    );

    /*//////////////////////////////////////////////////////////////
                            CUSTOM ERRORS
    //////////////////////////////////////////////////////////////*/

    // should not send eth directly to this contract, use swapAndDeposit function
    error NotPayable();

    // Minimum amount of eth sent when deposit
    // 0x4b1175db
    error TooLittleEthError();

    // minimum amount of shares not met on swap and deposit
    // 0x8517304e
    error MinSharesSlippageError();

    // Minimum amount of shares sent on withdraw
    // 0xe8471aeb
    error TooLittleSharesError();

    // minimum amount of shares not met on withdraw and swap
    // 0xfe0d2edb
    error MinEthSlippageError();

    // transferFrom failed while withdrawing
    error TransferFromFailed();

    // failed to transfer eth back to user after withdraw and swap
    error FailedToSendEth();

    /*//////////////////////////////////////////////////////////////
                    VARIABLES & EXTERNAL CONTRACTS
    //////////////////////////////////////////////////////////////*/

    // eth mainnet wsteth
    IWstETH internal immutable _WSTETH = IWstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
    // eth mainnet steth
    IERC20 internal immutable _STETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);

    // eth mainnet reth
    IERC20 internal immutable _RETH = IERC20(0xae78736Cd615f374D3085123A210448E74Fc6393);

    // eth mainnet sfrxeth
    IsfrxETH internal immutable _SFRXETH = IsfrxETH(0xac3E018457B222d93114458476f3E3416Abbe38F);
    // eth mainnet frxeth
    IERC20 internal immutable _FRXETH = IERC20(0x5E8422345238F34275888049021821E8E08CAa1f);

    // all the curve pools needed for swaps
    ICurvePool1 internal immutable _ETHTOSTETH = ICurvePool1(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
    ICurvePool2 internal immutable _ETHTORETH = ICurvePool2(0x0f3159811670c117c372428D4E69AC32325e4D0F);
    ICurvePool1 internal immutable _ETHTOFRXETH = ICurvePool1(0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577);

    // curve tryLSD mainnet pool
    ICurvePool2 internal immutable _TRYLSD = ICurvePool2(0x2570f1bD5D2735314FC102eb12Fc1aFe9e6E7193);

    // Used to prevent a loop where pool would send eth to the gateway and trigger a deposit
    bool internal _startedWithdraw;

    /*//////////////////////////////////////////////////////////////
                            CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    constructor() {
        // unlimited approve will be used to add liquidity to the tryLSD pool
        _WSTETH.approve(address(_TRYLSD), type(uint256).max);
        _RETH.approve(address(_TRYLSD), type(uint256).max);
        _SFRXETH.approve(address(_TRYLSD), type(uint256).max);

        // unlimited approve will be used to wrap steth to wsteth
        _STETH.approve(address(_WSTETH), type(uint256).max);
        // unlimited approve will be used to wrap frxeth to sfrxeth
        _FRXETH.approve(address(_SFRXETH), type(uint256).max);

        // unlimited approve will be used to swap steth to eth
        _STETH.approve(address(_ETHTOSTETH), type(uint256).max);
        // unlimited approve will be used to swap reth to eth
        _RETH.approve(address(_ETHTORETH), type(uint256).max);
        // unlimited approve will be used to swap frxeth to eth
        _FRXETH.approve(address(_ETHTOFRXETH), type(uint256).max);
    }

    /*//////////////////////////////////////////////////////////////
                            PAYABLE LOGIC
    //////////////////////////////////////////////////////////////*/

    function handleReceive() public payable {
        // should not send eth directly to this contract, use swapAndDeposit function
        if (_startedWithdraw == false) revert NotPayable();

        return;
    }

    fallback() external payable {
        handleReceive();
    }

    receive() external payable {
        handleReceive();
    }

    /*//////////////////////////////////////////////////////////////
                            DEPOSIT LOGIC
    //////////////////////////////////////////////////////////////*/

    function calculatePoolShares(uint256 depositAmount) public view returns (uint256 shares) {
        uint256 singleSwapAmount = depositAmount / 3;
        // we do this to avoid collecting dust due to rounding
        uint256 lastSwapAmount = depositAmount / 3 + (depositAmount % 3);

        // for get_dy asset 0 is eth, asset 1 is steth
        uint256 stethAmount = _ETHTOSTETH.get_dy(0, 1, singleSwapAmount);
        // calculate the amount of wsteth we get for stethAmount of eth
        uint256 wstethAmount = _WSTETH.getWstETHByStETH(stethAmount);
        // for get_dy asset 0 is eth, asset 1 is reth
        uint256 rethAmount = _ETHTORETH.get_dy(0, 1, singleSwapAmount);
        // for get_dy asset 0 is eth, asset 1 is frxeth
        uint256 frxethAmount = _ETHTOFRXETH.get_dy(0, 1, lastSwapAmount);
        // calculate the amount of sfrxeth we get for frxethAmount of eth
        uint256 sfrxethAmount = _SFRXETH.convertToShares(frxethAmount);

        // finally calculate the amount of pool shares we get for the 3 tokens
        shares = _TRYLSD.calc_token_amount([wstethAmount, rethAmount, sfrxethAmount], true);
    }

    function swapAndDeposit(address owner, uint256 minShares) public payable returns (uint256 shares) {
        // should send more than 0 eth
        if (msg.value == 0) revert TooLittleEthError();

        uint256 singleSwapAmount = msg.value / 3;
        // we do this to avoid collecting dust due to rounding
        uint256 lastSwapAmount = msg.value / 3 + (msg.value % 3);

        // exchange from eth to steth, target amount and minAmount (for slippage)
        uint256 stethAmount = _ETHTOSTETH.exchange{value: singleSwapAmount}(
            0,
            1,
            singleSwapAmount,
            0 // min amount set to 0 because we check pool shares for slippage
        );
        // then wrap to wsteth
        uint256 wstethAmount = _WSTETH.wrap(stethAmount);
        // exchange from eth to steth, target amount and minAmount (for slippage)
        uint256 rethAmount = _ETHTORETH.exchange_underlying{value: singleSwapAmount}(
            0,
            1,
            singleSwapAmount,
            0 // min amount set to 0 because we check pool shares for slippage
        );
        // exchange from eth to steth, target amount and minAmount (for slippage)
        uint256 frxethAmount = _ETHTOFRXETH.exchange{value: lastSwapAmount}(
            0,
            1,
            lastSwapAmount,
            0 // min amount set to 0 because we check pool shares for slippage
        );
        // then wrap to sfrxeth
        uint256 sfrxethAmount = _SFRXETH.deposit(frxethAmount, address(this));

        // add liquidity to pool
        shares = _TRYLSD.add_liquidity(
            [wstethAmount, rethAmount, sfrxethAmount],
            0, // min shares set to 0 because I check myself for slippage
            false,
            owner
        );

        // Check slippage
        if (shares <= minShares) revert MinSharesSlippageError();

        // emit deposit event
        emit Deposit(msg.sender, owner, msg.value, shares);
    }

    /*//////////////////////////////////////////////////////////////
                            WITHDRAW LOGIC
    //////////////////////////////////////////////////////////////*/

    function calculateEth(uint256 shares) public view returns (uint256 ethAmount) {
        uint256 totalSupply = _TRYLSD.totalSupply();

        uint256 wstethAmount = (_TRYLSD.balances(0) * shares) / totalSupply;
        uint256 rethAmount = (_TRYLSD.balances(1) * shares) / totalSupply;
        uint256 sfrxethAmount = (_TRYLSD.balances(2) * shares) / totalSupply;

        // calculate the amount of eth we get for singleSwapAmount of wsteth
        // for get_dy asset 0 is eth, asset 1 is frxeth
        ethAmount = _ETHTOSTETH.get_dy(1, 0, _WSTETH.getStETHByWstETH(wstethAmount));
        // calculate the amount of eth we get for singleSwapAmount of reth
        // for get_dy asset 0 is eth, asset 1 is frxeth
        ethAmount += _ETHTORETH.get_dy(1, 0, rethAmount);
        // calculate the amount of eth we get for singleSwapAmount of sfrxeth
        // for get_dy asset 0 is eth, asset 1 is frxeth
        ethAmount += _ETHTOFRXETH.get_dy(1, 0, _SFRXETH.convertToAssets(sfrxethAmount));
    }

    function withdrawAndSwap(address receiver, uint256 shares, uint256 minEth)
        public
        payable
        returns (uint256 ethAmount)
    {
        // this variable is to prevent a loop where pool would send eth to the gateway and trigger a deposit
        _startedWithdraw = true;

        // should send more than 0 shares
        if (shares == 0) revert TooLittleSharesError();

        bool success = _TRYLSD.transferFrom(msg.sender, address(this), shares);

        // this might be useless as transferFrom will revert itself if it fails
        if (success == false) revert TransferFromFailed();

        uint256[3] memory amounts =
            _TRYLSD.remove_liquidity(shares, [uint256(0), uint256(0), uint256(0)], false, address(this));

        // unwrap wsteth to steth
        uint256 stethAmount = _WSTETH.unwrap(amounts[0]);
        // exchange steth to eth
        uint256 stethToEthAmount = _ETHTOSTETH.exchange(
            1, // from steth
            0, // to eth
            stethAmount, // amount we got from unwrapping wsteth
            0 // min amount set to 0 because we check final eth amount for slippage
        );

        // exchange reth to eth
        uint256 rethToEthAmount = _ETHTORETH.exchange_underlying(
            1, // from reth
            0, // to eth
            amounts[1],
            0 // min amount set to 0 because we check final eth amount for slippage
        );

        // redeem frxeth from sfrxeth
        uint256 frxethAmount = _SFRXETH.redeem(amounts[2], address(this), address(this));
        // exchange frxeth to eth
        uint256 frxethToEthAmount = _ETHTOFRXETH.exchange(
            1, // from frxeth
            0, // to eth
            frxethAmount,
            0 // min amount set to 0 because we check final eth amount for slippage
        );

        ethAmount = stethToEthAmount + rethToEthAmount + frxethToEthAmount;

        // Check slippage
        if (ethAmount <= minEth) revert MinEthSlippageError();

        (bool sent,) = receiver.call{value: ethAmount}("");

        if (sent == false) revert FailedToSendEth();

        // emit withdraw event
        emit Withdraw(msg.sender, receiver, msg.sender, ethAmount, shares);

        // this variable is to prevent a loop where pool would send eth to the gateway and trigger a deposit
        _startedWithdraw = false;
    }
}

// File: lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
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);
}

// File: src/interfaces/ICurvePool.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";

interface ICurvePool1 is IERC20 {
    function get_p() external view returns (uint256);

    function get_dy(int128 from, int128 to, uint256 from_amount) external view returns (uint256);

    function exchange(int128 from, int128 to, uint256 from_amount, uint256 min_to_amount)
        external
        payable
        returns (uint256 amount);

    function price_oracle() external view returns (uint256 price);
}

interface ICurvePool2 is IERC20 {
    function get_p() external view returns (uint256);

    function get_dy(uint256 from, uint256 to, uint256 from_amount) external view returns (uint256);

    // Exchange using WETH by default
    function exchange(uint256 from, uint256 to, uint256 from_amount, uint256 min_to_amount)
        external
        payable
        returns (uint256 amount);

    // Exchange using ETH by default
    function exchange_underlying(uint256 from, uint256 to, uint256 from_amount, uint256 min_to_amount)
        external
        payable
        returns (uint256 amount);

    function add_liquidity(uint256[3] memory amounts, uint256 min_mint_amount, bool use_eth, address receiver)
        external
        payable
        returns (uint256 shares);

    function remove_liquidity(uint256 shares, uint256[3] memory minAmounts, bool use_eth, address receiver)
        external
        payable
        returns (uint256[3] memory amounts);

    function calc_token_amount(uint256[3] memory amounts, bool deposit) external view returns (uint256 amount);

    function totalSupply() external view returns (uint256 totalSupply);

    function balances(uint256 token) external view returns (uint256 balance);
}

// File: src/interfaces/IsfrxETH.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";

interface IsfrxETH is IERC20 {
    function convertToShares(uint256 assets) external view returns (uint256 shares);

    function convertToAssets(uint256 shares) external view returns (uint256 assets);

    function deposit(uint256 assets, address owner) external returns (uint256 shares);

    function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
}

// File: src/interfaces/IWstETH.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";

interface IWstETH is IERC20 {
    function getStETHByWstETH(uint256 wstETHAmount) external view returns (uint256 stETHAmount);

    function getWstETHByStETH(uint256 stETHAmount) external view returns (uint256 wstETHAmount);

    function stEthPerToken() external view returns (uint256 stETHAmount);

    function tokensPerStEth() external view returns (uint256 wstETHAmount);

    function wrap(uint256 stETHAmount) external returns (uint256 wstETHAmount);

    function unwrap(uint256 wstETHAmount) external returns (uint256 stETHAmount);
}