contract SatoshiExchange is AccessControl, GSNRecipientSignature {
    using SafeERC20 for ISatoshiToken;
    using SafeERC20 for IERC20Detailed;
    using SafeERC20 for IERC20Detailed;
    using SafeERC20 for IERC20;
    using SafeMath for uint256;

    // Satoshi token Contract
    ISatoshiToken private _satoshiToken;

    // Satoshi Price Feeder Contract
    ISatoshiPriceFeeder private _satoshiPriceFeed;

    // Satoshi Allotment Contract
    ISatoshiAllotment private _satoshiAllotment;

    // Default plan id for alloment, set to max uint value
    uint256 public _defaultPlanId = ~uint256(0);

    // Direct Buy fee, no fee for allotments
    uint256 public _buyFeePercentInWei;

    // Sell fee
    uint256 public _sellFeePercentInWei;

    // Flag for enabling/disabling direct buy of satoshi tokens
    bool public _enableDirectBuying = false;

    // List of supported USD stable coins
    mapping(address => bool) private _supportedTokens;

    // Withdrawal Wallet address
    address payable private _wallet;

    // Event details
    event TokenAdded(address indexed token);
    event TokenRemoved(address indexed token);
    event TokenContractChanged(address indexed newAddress);
    event PriceFeederContractChanged(address indexed newAddress);
    event AllotmentContractChanged(address indexed newAddress);
    event SatoshiAllotmentBought(address indexed account,
        uint64 indexed id,
        uint256 amountInWei,
        address indexed token,
        uint256 tokenAmount,
        uint256 timestamp);
    event SatoshiSold(address indexed account,
        uint256 amountInWei,
        address indexed token,
        uint256 tokenAmount,
        uint256 feeInWei,
        uint256 timestamp);
    event SatoshiBought(address indexed account,
        uint256 amountInWei,
        address indexed token,
        uint256 tokenAmount,
        uint256 timestamp);
    event BuyFeeUpdated(uint256 newFeePercentInWei, uint256 timestamp);
    event SellFeeUpdated(uint256 newFeePercentInWei, uint256 timestamp);
    event ChangedAllotmentPlan(uint64 planId, uint256 timestamp);
    event ChangedDirectBuying(bool flag, uint256 timestamp);
    event WalletChanged(address wallet);
    event TokensWithdrawn(address indexed token, uint256 amount, uint256 timestamp);
    event EthWithdrawn(uint256 balance, uint256 timestamp);
    event TokenDeposited(address indexed account, string depositKey, uint256 depositAmount, address token, uint256 timestamp);
    event EthDeposited(address indexed account, string depositKey, uint256 depositAmount, uint256 timestamp);

    // Supported token validity modifier
    modifier onlySupportedToken(address token) {
        require(isTokenSupported(token), "SatoshiExchange: token not supported");
        _;
    }

    /**
     * @dev Initialize the contract with Satoshi token address,
     * support stable coin tokens for exchange and the base price of Satoshi token.
     *
     * @param satoshiToken - Satoshi Token address
     * @param satoshiPriceFeeder - Satoshi Price Feeder Contract address
     * @param satoshiAllotment - Satoshi Allotment Contract address
     * @param defaultPlanId - Default allotment plan id, could be set
     * to ~uint256(0) to set the plan later
     * @param buyFeePercentInWei - Direct buy fee percent in wei
     * @param sellFeePercentInWei - Sell fee percent in wei
     * @param enableDirectBuying - Flag to enable/disable direct buying
     * @param supportedTokens - Array of supported token address for exchange
     * @param trustedSigner - Trusted signer for GSN
     */
    constructor(address satoshiToken,
            address satoshiPriceFeeder,
            address satoshiAllotment,
            uint256 defaultPlanId,
            uint256 buyFeePercentInWei,
            uint256 sellFeePercentInWei,
            bool enableDirectBuying,
            address[] memory supportedTokens,
            address trustedSigner
        ) public GSNRecipientSignature(trustedSigner) {

        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());

        require(satoshiToken != address(0), "SatoshiExchange: token is zero address");
        require(satoshiPriceFeeder != address(0), "SatoshiExchange: price Feeder is zero address");
        require(satoshiAllotment != address(0), "SatoshiExchange: allotment is zero address");

        // Set Satoshi ERC20 token Contract
        _satoshiToken = ISatoshiToken(satoshiToken);

        // Set Satoshi Price Feeder contract
        _satoshiPriceFeed = ISatoshiPriceFeeder(satoshiPriceFeeder);

        // Set Satoshi Allotment contract
        _satoshiAllotment = ISatoshiAllotment(satoshiAllotment);

        // Set direct buy fee percentage
        _buyFeePercentInWei = buyFeePercentInWei;

        // Set sell fee percentage
        _sellFeePercentInWei = sellFeePercentInWei;

        // Set direct buying flag
        _enableDirectBuying = enableDirectBuying;

        // Set the stable coin tokens supported for exchange
        for (uint256 i = 0; i < supportedTokens.length; i++){
            _supportedTokens[supportedTokens[i]] = true;
            emit TokenAdded(supportedTokens[i]);
        }

        // Set default plan id
        _defaultPlanId = defaultPlanId;
    }

    /**
     * @dev Sets the trusted signer that is going to be producing signatures to approve relayed calls.
     *
     * @param trustedSigner - Address of the trusted signer for GSN
     *
     * Requirements:
     *
     * - the caller must have the `DEFAULT_ADMIN_ROLE`
     */
    function setTrustedSigner(address trustedSigner) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiToken: must have admin role to change trustedSigner");
        super.setTrustedSigner(trustedSigner);
    }

    /**
    * @dev Change the Satoshi Token contract
    *
    * @param newAddress New Address of the token
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeSatoshiTokenContract(address newAddress) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change satoshi token contract"
        );
        require(newAddress != address(0), "SatoshiExchange: token is zero address");

        // Change Satoshi ERC20 token Contract
        _satoshiToken = ISatoshiToken(newAddress);

        emit TokenContractChanged(newAddress);
    }

    /**
    * @dev Change the Satoshi Price Feeder contract
    *
    * @param newAddress New Address of the price feeder contract
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeSatoshiPriceFeederContract(address newAddress) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change price feeder contract"
        );
        require(newAddress != address(0), "SatoshiExchange: price feeder is zero address");

        // Change Satoshi Price Feeder contract
        _satoshiPriceFeed = ISatoshiPriceFeeder(newAddress);

        emit PriceFeederContractChanged(newAddress);
    }

    /**
    * @dev Change the Satoshi Allotment contract
    *
    * @param newAddress New Address of the allotment contract
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeSatoshiAllotmentContract(address newAddress) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change allotment contract"
        );
        require(newAddress != address(0), "SatoshiExchange: allotment is zero address");

        // Change Satoshi Price Feeder contract
        _satoshiAllotment = ISatoshiAllotment(newAddress);

        emit AllotmentContractChanged(newAddress);
    }

    /**
    * @dev Change direct buying fee
    *
    * @param newFeePercentInWei Updated fee for buying satoshi
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeBuyFee(uint256 newFeePercentInWei) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change change buy fee"
        );
        // Change Satoshi direct buy fee
        _buyFeePercentInWei = newFeePercentInWei;

        emit BuyFeeUpdated(newFeePercentInWei, block.timestamp);
    }

    /**
    * @dev Change selling fee
    *
    * @param newFeePercentInWei Updated fee for sell
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeSellFee(uint256 newFeePercentInWei) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change change sell fee"
        );
        // Change Satoshi sellingfee
        _sellFeePercentInWei = newFeePercentInWei;

        emit SellFeeUpdated(newFeePercentInWei, block.timestamp);
    }

    /**
    * @dev Returns whether given token is supported for subscriptions or not
    *
    * @param token Address of the token to be checked
    *
    * @return bool - Whether token is supported or not
    */
    function isTokenSupported(address token) public view returns(bool) {
        return _supportedTokens[token];
    }

    /**
    * @dev Allows admin to add supported token
    *
    * @param token Token address to be added
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function addToken(address token) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to add token"
        );
        require(token != address(0), "SatoshiExchange: token is zero address");
        require(!_supportedTokens[token],
            "SatoshiExchange: token already supported"
        );
        _supportedTokens[token] = true;
        emit TokenAdded(token);
    }

    /**
    * @dev Allows admin to remove any supported token from the list
    *
    * @param token Token to be removed
    * Requirements:
    *
    * - only supported tokens
    * - the caller must have the admin role.
    */
    function removeToken(address token) external onlySupportedToken(token) {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to remove token"
        );

        _supportedTokens[token] = false;
        emit TokenRemoved(token);
    }

    /**
    * @dev Adjust decimal precision from given value to another precision
    *
    * @param value - Value which needs precision adjustment
    * @param fromDecimals - Present decimal precision
    * @param toDecimals - Resulting decimal precision
    *
    * @return uint256 - Adjusted value to toDecimals precision
    */
    function adjustPrecision(uint256 value, uint8 fromDecimals, uint8 toDecimals) public pure returns (uint256) {
        if (fromDecimals == toDecimals) {
            return value;
        } else if (fromDecimals < toDecimals) {
            return value.mul(uint256(10)**(toDecimals - fromDecimals));
        } else {
            return value.div(uint256(10)**(fromDecimals - toDecimals));
        }
    }

    /**
    * @dev Convert given amount of Satoshi tokens to the value of stablecoin token,
    * and by also considering the current price of satoshi token.
    *
    * @param amountInWei - Amount of Satoshi Token to be bought
    * @param token - Supported stablecoin address
    *
    * @return uint256 - Converted value of stablecoin tokens
    */
    function convertSatoshiToTokenAmount(uint256 amountInWei, address token) public view returns (uint256) {
        // Decimal count of Stablecoin token to be received, for supporting
        // stable coins of different decimal places
        uint8 tokenDecimals = IERC20Detailed(token).decimals();
        uint8 satoshiDecimals = _satoshiToken.decimals();

        // Protection for not losing too much of decimals in conversion
        require(tokenDecimals >= 6, "SatoshiExchange: token should have greater that 6 decimals");

        // When there is difference in decimal precision, we might
        // lose precision but its an expected result. Only downside is,
        // a slightly less amount of stablecoin token shall be received/transferred.
        uint256 satoshiPriceInTokenDecimals = adjustPrecision(_satoshiPriceFeed.getPriceInWei(), satoshiDecimals, tokenDecimals);
        uint256 amountInWeiInTokenDecimals = adjustPrecision(amountInWei, satoshiDecimals, tokenDecimals);

        return amountInWeiInTokenDecimals.mul(satoshiPriceInTokenDecimals).div(uint256(10)**tokenDecimals);
    }

    /**
    * @dev Set default allotment plan ID
    *
    * @param planId - Allotment plan ID
    * Requirements:
    *
    * - the caller must have the admin role.
    * - Verify whether the allotment plan id is valid
    */
    function setDefaultAllotmentPlan(uint64 planId) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change allotment contract"
        );
        // Note: When plan ID is not present, then this will cause a revert
        require(_satoshiAllotment.isAllotmentPlanValid(planId) == true, "SatoshiExchange: Deleted allotment plan ID");

        _defaultPlanId = planId;
        emit ChangedAllotmentPlan(planId, block.timestamp);
    }

    /**
    * @dev Set direct buying
    *
    * @param flag - Flag to enable/disable direct buying
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function setDirectBuying(bool flag) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change direct buy flag"
        );

        _enableDirectBuying = flag;
        emit ChangedDirectBuying(flag, block.timestamp);
    }

    /**
    * @dev Buy Satoshi allottment by paying equivalant supported stablecoins
    * An allotment with default plan is granted. Tokens are released based
    * on the allotment plan release cycle. Users should call release() in
    * allotment contract to withdraw the tokens available for release.
    *
    * @param amountInWei - Amount of Satoshi Token to be bought
    * @param token - Supported stablecoin address for making payment
    * Requirements:
    *
    * - amountInWei should be greated than 0
    * - token address should be non-zero
    */
    function buySatoshiAllotment(uint256 amountInWei, address token) external onlySupportedToken(token) {
        // Verify whether default plan is configured
        require(_defaultPlanId != ~uint256(0), "SatoshiExchange: default plan not configured");

        require(amountInWei > 0, "SatoshiExchange: amount is zero");
        require(token != address(_satoshiToken), "SatoshiExchange: cannot buy using satoshi");

        uint256 tokenAmount = convertSatoshiToTokenAmount(amountInWei, token);

        // Receive respective amount of supported stablecoin tokens from the msg.sender
        IERC20(token).safeTransferFrom(_msgSender(), address(this), tokenAmount);

        // Grant allotment to the msg.sender
        uint64 id = _satoshiAllotment.grantAllotment(
                        _msgSender(),
                        amountInWei,
                        (uint32)(block.timestamp),
                        _defaultPlanId
                    );

        emit SatoshiAllotmentBought(_msgSender(), id, amountInWei, token, tokenAmount, block.timestamp);
    }

    /**
    * @dev Sell Satoshi token to receive equivalant supported stablecoins
    *
    * @param amountInWei - Amount of Satoshi Token to be sold
    * @param token - Supported stablecoin address for receiving payment
    * Requirements:
    *
    * - amountInWei should be greated than 0
    * - token address should be non-zero
    */
    function sellSatoshi(uint256 amountInWei, address token) external onlySupportedToken(token) {
        require(amountInWei > 0, "SatoshiExchange: amount is zero");
        require(token != address(_satoshiToken), "SatoshiExchange: cannot sell using satoshi");

        uint256 fee = amountInWei.mul(_sellFeePercentInWei).div(100).div(10**18);
        // Apply fees
        uint256 amountAfterFees = amountInWei - fee;

        uint256 tokenAmount = convertSatoshiToTokenAmount(amountAfterFees, token);

        require(tokenAmount > 0, "SatoshiExchange: converted amount is zero");

        // Burn Satoshi Token from msg.sender account
        _satoshiToken.burnFrom(_msgSender(), amountInWei);

        // Transfer respective amount of supported stablecoin tokens to the msg.sender
        IERC20(token).safeTransfer(_msgSender(), tokenAmount);

        emit SatoshiSold(_msgSender(), amountInWei, token, tokenAmount, fee, block.timestamp);
    }

    /**
    * @dev Buy Satoshi directly
    *
    * @param amountInWei - Amount of Satoshi Token to be bought
    * @param token - Supported stablecoin address for making payment
    *
    * Requirements:
    *
    * - amountInWei should be greated than 0
    * - direct buy should have been enabled
    */
    function buySatoshiDirect(uint256 amountInWei, address token) external onlySupportedToken(token) {
        require(amountInWei > 0, "SatoshiExchange: amount is zero");
        require(token != address(_satoshiToken), "SatoshiExchange: cannot buy using satoshi");

        require(_enableDirectBuying == true, "SatoshiExchange: direct buying is disabled");

        uint256 fee = amountInWei.mul(_buyFeePercentInWei).div(100).div(10**18);
        // Apply fees
        uint256 amountAfterFees = amountInWei + fee;

        uint256 tokenAmount = convertSatoshiToTokenAmount(amountAfterFees, token);

        // Receive respective amount of supported stablecoin tokens from the msg.sender
        IERC20(token).safeTransferFrom(_msgSender(), address(this), tokenAmount);

        // Mint Satoshi Token to msg.sender account
        _satoshiToken.mint(_msgSender(), amountInWei);

        emit SatoshiBought(_msgSender(), amountInWei, token, tokenAmount, block.timestamp);
    }

    /**
    * @dev Allows owner to change the withdrawal wallet address
    *
    * @param newWallet New wallet address
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function changeWallet(address payable newWallet) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to change wallet"
        );
        require(newWallet != address(0), "SatoshiExchange: wallet is zero address");

        _wallet = newWallet;
        emit WalletChanged(newWallet);
    }

    /**
    * @dev Deposit Satoshi tokens of the user and log an event
    *
    * @param depositKey - An unique key to identify the desposit
    * @param depositAmount - Amount of tokens to transfer from user account
    * @param token - Address of the supported token
    *
    * Requirements:
    * - only supported tokens are allowed for deposit
    * - deposit amount should be greater than zero
    *
    */
    function depositTokens(
        string calldata depositKey,
        uint256 depositAmount,
        address token) external onlySupportedToken(token) {

        require(depositAmount != 0, "SatoshiDeposit: deposit amount is zero");

        // Receive respective amount of supported tokens from the msg.sender
        IERC20(token).safeTransferFrom(_msgSender(), address(this), depositAmount);

        emit TokenDeposited(_msgSender(), depositKey, depositAmount, token, block.timestamp);
    }

    /**
    * @dev Deposit ether of the user and log an event
    *
    * @param depositKey - An unique key to identify the desposit
    *
    * Requirements:
    * - only supported tokens are allowed for deposit
    * - deposit amount should be greater than zero
    *
    */
    function depositEth(string calldata depositKey) external payable {

        require(msg.value != 0, "SatoshiDeposit: ether deposit amount is zero");

        emit EthDeposited(_msgSender(), depositKey, msg.value, block.timestamp);
    }

    /**
    * @dev Allows owner to withdraw tokens from contract
    *
    * @param token Token for which funds will be withdrawn
    * @param amount Amount of tokens to be withdrawn
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function withdrawTokens(uint256 amount, address token) external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to withdraw tokens"
        );
        require(token != address(0), "SatoshiExchange: token is zero address");
        require(_wallet != address(0), "SatoshiExchange: wallet is zero address");

        require(amount > 0, "SatoshiExchange: amount is zero");

        IERC20(token).transfer(
            _wallet,
            amount
        );
        emit TokensWithdrawn(token, amount, block.timestamp);
    }

    /**
    * @dev Allows owner to withdraw ether from contract
    *
    *
    * Requirements:
    *
    * - the caller must have the admin role.
    */
    function withdrawEth() external {
        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
            "SatoshiExchange: must have admin role to withdraw eth"
        );

        require(_wallet != address(0), "SatoshiExchange: wallet is zero address");

        uint256 balance = address(this).balance;

        _wallet.transfer(address(this).balance);

        emit EthWithdrawn(balance, block.timestamp);
    }
}
/**
 * @dev This contract handles exchange of Satoshi Tokens to supported stablecoins
 * and vice-versa
 */
function adjustPrecision(uint256 value, uint8 fromDecimals, uint8 toDecimals) public pure returns (uint256) {
    if (fromDecimals == toDecimals) {
        return value;
    } else if (fromDecimals < toDecimals) {
        return value.mul(uint256(10)**(toDecimals - fromDecimals));
    } else {
        return value.div(uint256(10)**(fromDecimals - toDecimals));
    }
}

/**
 * @dev Adjust decimal precision from given value to another precision
 *
 * @param value - Value which needs precision adjustment
 * @param fromDecimals - Present decimal precision
 * @param toDecimals - Resulting decimal precision
 *
 * @return uint256 - Adjusted value to toDecimals precision
 */