// File: contracts/swap/ChainlinkSwap.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;
import "./BaseSwap.sol";

contract ChainlinkSwap is BaseSwap {

    /// @param _commodityToken the commodity token
    /// @param _stableToken the stable token
    /// @param _commodityChainlinkAddress chainlink price feed address for commodity
    /// @param _newDexSettings dexsettings 
    constructor(
        address _commodityToken,
        address _stableToken,
        address _commodityChainlinkAddress,
        SwapLib.DexSetting memory _newDexSettings
    ) {
        require(
            _newDexSettings.dexAdmin != address(0),
            "Invalid address"
        );
        require(_newDexSettings.unitMultiplier > 0, "Invalid _unitMultiplier");
        dexData.commodityToken = _commodityToken;
        dexData.stableToken = _stableToken;
        dexSettings.comdexName = _newDexSettings.comdexName;
        dexSettings.tradeFee = _newDexSettings.tradeFee;
        dexSettings.dexAdmin = _newDexSettings.dexAdmin;
        dexSettings.unitMultiplier = _newDexSettings.unitMultiplier; 
        dexSettings.stableToUSDPriceFeed = _newDexSettings.stableToUSDPriceFeed;
        dexSettings.buySpotDifference = _newDexSettings.buySpotDifference;
        dexSettings.sellSpotDifference = _newDexSettings.sellSpotDifference;

        priceFeed = AggregatorV3Interface(_commodityChainlinkAddress);
        stableTokenPriceFeed = AggregatorV3Interface(_newDexSettings.stableToUSDPriceFeed);
    }

    /// @notice Allows Swaps from commodity token to another token and vice versa,
    /// @param _amountIn Amount of tokens user want to give for swap (in decimals of _from token)
    /// @param _from token that user wants to spend
    /// @param _to token that user wants in result of swap

    function swap(
        uint256 _amountIn,
        address _from,
        address _to
    ) external virtual whenNotPaused {
        require(_amountIn > 0, "wrong amount");
        require(
            (_from == dexData.commodityToken && _to == dexData.stableToken) ||
                (_to == dexData.commodityToken && _from == dexData.stableToken),
            "wrong pair"
        );

        uint256 amountFee = (_amountIn * dexSettings.tradeFee) / (10**10); // 8 decimals for fee, 100 for percentage

        if (_from == dexData.commodityToken) {
            uint256 commodityAmount = _amountIn - amountFee;
            uint256 stableAmount = getAmountOut(commodityAmount, false);
            if (dexData.reserveStable < stableAmount)
                emit LowstableTokenalance(dexData.stableToken, dexData.reserveStable);
            require(dexData.reserveStable >= stableAmount, "not enough liquidity");
            
            TransferHelper.safeTransferFrom(
                dexData.commodityToken,
                msg.sender,
                address(this),
                _amountIn
            );
            TransferHelper.safeTransfer(dexData.stableToken, msg.sender, stableAmount);

            dexData.reserveCommodity = dexData.reserveCommodity + commodityAmount;
            dexData.reserveStable = dexData.reserveStable - stableAmount;
            dexData.totalFeeCommodity = dexData.totalFeeCommodity + amountFee;
            emit Swapped(msg.sender, _amountIn, stableAmount, SwapLib.SELL_INDEX);
        } else {
            uint256 stableAmount = _amountIn - amountFee;
            uint256 commodityAmount = getAmountOut(stableAmount, true);

            if (dexData.reserveCommodity < commodityAmount)
                emit LowstableTokenalance(dexData.commodityToken, dexData.reserveCommodity);
            require(dexData.reserveCommodity >= commodityAmount, "not enough liquidity");

            TransferHelper.safeTransferFrom(
                dexData.stableToken,
                msg.sender,
                address(this),
                _amountIn
            );
            TransferHelper.safeTransfer(dexData.commodityToken, msg.sender, commodityAmount);

            dexData.reserveCommodity = dexData.reserveCommodity - commodityAmount;
            dexData.reserveStable = dexData.reserveStable + stableAmount;
            dexData.totalFeeStable = dexData.totalFeeStable + amountFee;
            emit Swapped(msg.sender, _amountIn, commodityAmount, SwapLib.BUY_INDEX);
        }
    }


    /// @notice adds liquidity for both assets
    /// @dev stableAmount should be = commodityAmount * price
    /// @param commodityAmount amount of tokens for commodity asset
    /// @param stableAmount amount of tokens for stable asset

    function addLiquidity(uint256 commodityAmount, uint256 stableAmount)
        external
        virtual
        onlyOwner
    {
        uint amount = getAmountOut(commodityAmount, false);
        require(
            amount == stableAmount,
            "amounts should be equal"
        );
        super._addLiquidity(commodityAmount, stableAmount);
    }

    /// @notice removes liquidity for both assets
    /// @dev stableAmount should be = commodityAmount * price
    /// @param commodityAmount amount of tokens for commodity asset
    /// @param stableAmount amount of tokens for stable asset

    function removeLiquidity(uint256 commodityAmount, uint256 stableAmount)
        external
        virtual
        onlyOwner
    {
        uint amount = getAmountOut(commodityAmount, false);//false flag to get sell price
        require(
            amount == stableAmount,
            "commodityAmount should be equal"
        );
        super._removeLiquidity(commodityAmount, stableAmount);
    }

    function getAmountOut(uint256 _amountIn, bool flag) public view returns(uint256){
        if(flag){//buy price for 1 unit of commdotiy token
            uint commodityUnitPrice = convertUSDToStable(getChainLinkFeedPrice());
            commodityUnitPrice = commodityUnitPrice + ((commodityUnitPrice * dexSettings.buySpotDifference) / 10000) ; // adding 1.12% from spot price
            uint256 commodityAmount = (_amountIn * (10**8)) / commodityUnitPrice;
            commodityAmount = SwapLib.normalizeAmount(commodityAmount, dexData.stableToken, dexData.commodityToken);
            return commodityAmount;
        }
        else{//sell price for 1 unit of commodity token
            uint256 stableAmount = (_amountIn * getChainLinkFeedPrice()) / (10**8);
            stableAmount = convertUSDToStable(stableAmount);
            stableAmount = stableAmount - ((stableAmount * dexSettings.sellSpotDifference)/(10000)); // deducting 1.04% out of spot price
            stableAmount = SwapLib.normalizeAmount(stableAmount, dexData.commodityToken, dexData.stableToken);
            return stableAmount;
        }
    }
}

// File: contracts/swap/BaseSwap.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

import "../utils/TransferHelper.sol";
import "../interfaces/ISwap.sol";
import "./../utils/Pausable.sol";
import "./../interfaces/IERC20.sol";

import {SwapLib} from "../lib/Lib.sol";

abstract contract BaseSwap is Ownable, ISwap, Pausable {
    using SwapLib for *;

    SwapLib.DexData public dexData;
    SwapLib.DexSetting public dexSettings;
    AggregatorV3Interface internal priceFeed;
    AggregatorV3Interface internal stableTokenPriceFeed;
    // uint256 public unitMultiplier; // will be used to convert feed price units to token price units

    modifier onlyComdexAdmin() {
        _onlyCommdexOwner();
        _;
    }

    function _onlyCommdexOwner() internal view{
       require(
            msg.sender == dexSettings.dexAdmin,
            "Caller is not comm-dex owner"
        );
    }

    /// @notice Adds liquidity for both assets
    /// @param commodityAmount amount of tokens for commodity asset
    /// @param stableAmount amount of tokens for stable asset

    function _addLiquidity(uint256 commodityAmount, uint256 stableAmount) internal {
        TransferHelper.safeTransferFrom(
            dexData.commodityToken,
            msg.sender,
            address(this),
            commodityAmount
        );
        TransferHelper.safeTransferFrom(
            dexData.stableToken,
            msg.sender,
            address(this),
            stableAmount
        );
        dexData.reserveCommodity = dexData.reserveCommodity + commodityAmount;
        dexData.reserveStable = dexData.reserveStable + stableAmount;
        emit LiquidityAdded(_msgSender(), commodityAmount, stableAmount);
    }

    /// @notice Removes liquidity for both assets
    /// @param commodityAmount amount of tokens for commodity asset
    /// @param stableAmount amount of tokens for stable asset

    function _removeLiquidity(uint256 commodityAmount, uint256 stableAmount) internal {
        TransferHelper.safeTransfer(dexData.commodityToken, _msgSender(), commodityAmount);
        TransferHelper.safeTransfer(dexData.stableToken, _msgSender(), stableAmount);
        dexData.reserveCommodity = dexData.reserveCommodity - commodityAmount;
        dexData.reserveStable = dexData.reserveStable - stableAmount;
        emit LiquidityRemoved(_msgSender(), commodityAmount, stableAmount);
    }

    /// @notice Allows to set trade fee for swap
    /// @param _newTradeFee updated trade fee, should be < 10 ** 8


    function setTradeFee(uint256 _newTradeFee) external onlyComdexAdmin {
        require(_newTradeFee < 10**8, "Wrong Fee!");
        dexSettings.tradeFee = _newTradeFee;
        emit TradeFeeChanged(_newTradeFee);
    }

    /// @notice Allows comm-dex admin to withdraw fee

    function withdrawFee() external onlyComdexAdmin {

        withdrawFeeHelper();

        emit FeeWithdraw(
            msg.sender,
            dexData.totalFeeCommodity,
            dexData.totalFeeStable
        );

        resetFees();
    }

    /// @notice Allows to set Chainlink feed address
    /// @param _chainlinkPriceFeed the updated chainlink price feed address

    function setChainlinkFeedAddress(address _chainlinkPriceFeed)
        external
        onlyComdexAdmin
    {
        priceFeed = AggregatorV3Interface(_chainlinkPriceFeed);
        emit ChainlinkFeedAddressChanged(_chainlinkPriceFeed);
    }

    /// @notice Allows to set comm-dex admin
    /// @param _updatedAdmin the new admin

    function setCommdexAdmin(address _updatedAdmin) external onlyComdexAdmin {
        require(
            _updatedAdmin != address(0) &&
                _updatedAdmin != dexSettings.dexAdmin,
            "Invalid Address"
        );
        dexSettings.dexAdmin = _updatedAdmin;
        emit ComDexAdminChanged(_updatedAdmin);
    }

    /// @notice allows Swap admin to withdraw reserves in case of emergency

    function emergencyWithdraw() external onlyOwner {

        withDrawReserveHelper();

        emit EmergencyWithdrawComplete(
            msg.sender,
            dexData.reserveCommodity,
            dexData.reserveStable
        );

        resetReserves();
    }

    /// @notice Allows comm-dex admin to empty dex, sends reserves to comm-dex admin and fee to comm-dex admin

    function withDrawAndDestory(address _to) external onlyComdexAdmin {

        // withdraw the reserves
        withDrawReserveHelper();
        // withdraw fees
        withdrawFeeHelper();

        emit withDrawAndDestroyed(
            msg.sender,
            dexData.reserveCommodity,
            dexData.reserveStable,
            dexData.totalFeeCommodity,
            dexData.totalFeeStable
        );

        selfdestruct(payable(_to));
    }

    function getChainLinkFeedPrice() internal view returns (uint256) {
        (
            ,
            /*uint80 roundID*/
            int256 price, /*uint startedAt*/ /*uint timeStamp*/ /*uint80 answeredInRound*/
            ,
            ,

        ) = priceFeed.latestRoundData();
        require(price >= 0, "ChainLink price error");

        return (uint256(price) * dexSettings.unitMultiplier) / (10**18); // converting feed price unit into token commodity units e.g 1 gram = 1000mg
    }

    function convertUSDToStable(uint _amount) internal view returns (uint256) {
        (
            ,
            /*uint80 roundID*/
            int256 price, /*uint startedAt*/ /*uint timeStamp*/ /*uint80 answeredInRound*/
            ,
            ,

        ) = stableTokenPriceFeed.latestRoundData();
        require(price >= 0, "ChainLink price error");

        return ((_amount * (1* (10**(8+5)) / uint256(price))))/ (10**5) ; // supporting 5 decimals on USD values
    }

    function withdrawFeeHelper() internal{
        address dexAdmin = dexSettings.dexAdmin;

        TransferHelper.safeTransfer(
            dexData.commodityToken,
            dexAdmin,
            dexData.totalFeeCommodity
        );
        TransferHelper.safeTransfer(
            dexData.stableToken,
            dexAdmin,
            dexData.totalFeeStable
        );
    }

    function withDrawReserveHelper() internal {
        address dexOwner = owner();
        TransferHelper.safeTransfer(
            dexData.commodityToken,
            dexOwner,
            dexData.reserveCommodity
        );
        TransferHelper.safeTransfer(
            dexData.stableToken,
            dexOwner,
            dexData.reserveStable
        );
    }

    function resetReserves() internal{
        dexData.reserveCommodity = 0;
        dexData.reserveStable = 0;
    }

    function resetFees() internal {
        dexData.totalFeeCommodity = 0;
        dexData.totalFeeStable = 0;
    }

    /// @notice pauses the Swap function

    function unpause() external onlyComdexAdmin {
        _unpause();
    }

    /// @notice unpause the Swap function

    function pause() external onlyComdexAdmin{
        _pause();
    }

    function updateUnitMultiplier(uint _unitMultiplier) external onlyOwner{
        require(_unitMultiplier > 0 , "Invalid  _unitMultiplier");
        dexSettings.unitMultiplier = _unitMultiplier;
        emit UnitMultiplierUpdated(_unitMultiplier);
    }

    function updateBuySpotDifference(uint _newDifference) external onlyComdexAdmin{
        dexSettings.buySpotDifference = _newDifference;
        emit BuySpotDifferenceUpdated(_newDifference);
    }
    
    function updateSellSpotDifference(uint _newDifference) external onlyComdexAdmin{
        dexSettings.sellSpotDifference = _newDifference;
        emit SellSpotDifferenceUpdated(_newDifference);
    }       
}

// File: contracts/utils/TransferHelper.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;

// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
library TransferHelper {
    function safeApprove(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0x095ea7b3, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::safeApprove: approve failed"
        );
    }

    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0xa9059cbb, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::safeTransfer: transfer failed"
        );
    }

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0x23b872dd, from, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::safeTransferFrom: transferFrom failed"
        );
    }

}

// File: contracts/interfaces/ISwap.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;

interface ISwap {
    event LowstableTokenalance(address Token, uint256 balanceLeft);
    event Swapped(
        address indexed sender,
        uint256 amountIn,
        uint256 amountOut,
        uint256 isSale
    );
    event LiquidityAdded(
        address indexed sender,
        uint256 commodityAmount,
        uint256 stableAmount
    );
    event LiquidityRemoved(
        address indexed sender,
        uint256 commodityAmount,
        uint256 stableAmount
    );
    event TradeFeeChanged(uint256 newTradeFee);
    event ComDexAdminChanged(address newAdmin);
    event EmergencyWithdrawComplete(
        address indexed sender,
        uint256 commodityAmount,
        uint256 stableAmount
    );
    event FeeWithdraw(address indexed sender, uint256 commodityAmount, uint256 stableAmount);
    event ChainlinkFeedAddressChanged(address newFeedAddress);
    event withDrawAndDestroyed(
        address indexed sender,
        uint256 reserveCommodity,
        uint256 reserveStable,
        uint256 feeA,
        uint256 feeB
    );

    event UnitMultiplierUpdated(uint256);
    event BuySpotDifferenceUpdated(uint256);
    event SellSpotDifferenceUpdated(uint256);
}

// File: contracts/utils/Pausable.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;


abstract contract Pausable {
  
    event Paused(address account);


    event Unpaused(address account);

    bool private _paused;

 
    constructor() {
        _paused = false;
    }


    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }


    modifier whenPaused() {
        _requirePaused();
        _;
    }

    function paused() public view virtual returns (bool) {
        return _paused;
    }


    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }


    function _requirePaused() internal view virtual {
        require(paused(), "Pausable: not paused");
    }

    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(msg.sender);
    }

  
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(msg.sender);
    }
}

// File: contracts/lib/Lib.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;

import "./../interfaces/IERC20.sol";
library ChainlinkLib {
    struct ApiInfo {
        string _apiUrl;
        string[2] _chainlinkRequestPath; //0 index contains buy and 1 contains sell
    }
    struct ChainlinkInfo {
        address chainlinkToken;
        address chainlinkOracle;
        address chianlinkPriceFeed;
        bool chainlinkFeedEnabled;
    }
}

library SwapLib {
    uint256 constant BUY_INDEX = 0; //index used to indicate a BUY trx
    uint256 constant SELL_INDEX = 1; //index used to indicate a SELL trx
    struct DexSetting {
        string comdexName;//name of the dex-pool
        uint256 tradeFee;//percentage fee deducted on each swap in 10**8 decimals
        address dexAdmin;//address responsible for certain admin functions e.g. addLiquidity
        uint256 rateTimeOut;//if expires swap will be paused 
        uint256 unitMultiplier;//to convert feed price units to commodity token units
        address stableToUSDPriceFeed; //chainlink feed for stable/usd conversion
        uint256 buySpotDifference; // % difference in buy spot price e.g 112 means 1.12%
        uint256 sellSpotDifference; // % difference in sell spot price e.g 104 means 1.04%
    }

    struct DexData {
        uint256 reserveCommodity;
        uint256 reserveStable;
        uint256 totalFeeCommodity; // storage that the fee of token A can be stored
        uint256 totalFeeStable; // storage that the fee of token B can be stored
        address commodityToken;
        address stableToken;
    }

    function normalizeAmount(uint _amountIn, address _from, address _to) internal view returns(uint256){
        uint fromDecimals = IERC20(_from).decimals();
        uint toDecimals = IERC20(_to).decimals();
        if(fromDecimals == toDecimals) return _amountIn;
        return fromDecimals > toDecimals ? _amountIn / (10**(fromDecimals-toDecimals)) : _amountIn * (10**(toDecimals - fromDecimals));
    }

    function checkFee(uint _fee) internal pure{
        require(_fee <10**8, "wrong fee amount");
    }

    function checkcommodityTokenddress(address _commodityToken, address _stableToken) internal pure{
        require(_commodityToken != address(0) && _stableToken != address(0),"invalid token");
    }
}

library PriceLib {
    struct PriceInfo {
        uint256[] rates; //= new uint256[](2);//0 index contains buy and 1 contains sell
        bytes32[] chainlinkRequestId; // = new bytes32[](2);//0 index contains buy and 1 contains sell
        uint256[] lastTimeStamp; //= new uint256[](2);//0 index contains buy and 1 contains sell
        uint256[] lastPriceFeed; //= new uint256[](2);//0 index contains buy and 1 contains sell
    }
}

// File: contracts/interfaces/IERC20.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.15;

interface IERC20{
    function decimals() external view returns (uint8);
}
// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (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 Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        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);
    }
}

// File: @chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol



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

interface AggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  // getRoundData and latestRoundData should both raise "No data present"
  // if they do not have data to report, instead of returning unset values
  // which could be misinterpreted as actual reported values.
  function getRoundData(uint80 _roundId)
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

  function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );
}

// File: @openzeppelin/contracts/utils/Context.sol



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