contract StockLiquiditator is ERC20, ERC20Detailed {
    using SafeMath for uint256;

    uint256 public cashDecimals;
    uint256 public stockTokenMultiplier;

    ERC20Detailed public cash;
    ERC20Detailed public stockToken;

    uint256 public stockToCashRate;
    uint256 public poolToCashRate;
    uint256 public cashValauationCap;

    string public url;

    event UrlUpdated(string _url);
    event ValuationCapUpdated(uint256 cashCap);
    event OwnerChanged(address indexed newOwner);
    event PoolRateUpdated(uint256 poolrate);
    event PoolTokensMinted(
        address indexed user,
        uint256 inputCashAmount,
        uint256 mintedPoolAmount
    );
    event PoolTokensBurnt(
        address indexed user,
        uint256 burntPoolAmount,
        uint256 outputStockAmount,
        uint256 outputCashAmount
    );
    event StockTokensRedeemed(
        address indexed user,
        uint256 redeemedStockToken,
        uint256 outputCashAmount
    );

    function() external {
        //fallback function
    }

    address payable public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Account not Owner");
        _;
    }

    constructor(
        address cashAddress,
        address stockTokenAddress,
        uint256 _stockToCashRate,
        uint256 cashCap,
        string memory name,
        string memory symbol,
        string memory _url
    ) public ERC20Detailed(name, symbol, 18) {
        require(
            msg.sender != address(0),
            "Zero address cannot be owner/contract deployer"
        );
        owner = msg.sender;
        require(
            stockTokenAddress != address(0),
            "stockToken is the zero address"
        );
        require(cashAddress != address(0), "cash is the zero address");
        require(_stockToCashRate != 0, "Stock to cash rate can't be zero");
        cash = ERC20Detailed(cashAddress);
        stockToken = ERC20Detailed(stockTokenAddress);
        cashDecimals = cash.decimals();
        stockTokenMultiplier = (10**uint256(stockToken.decimals()));
        stockToCashRate = ((10**cashDecimals).mul(_stockToCashRate)).div(1e18);
        updatePoolRate();
        updateCashValuationCap(cashCap);
        updateURL(_url);
    }

    function updateURL(string memory _url)
        public
        onlyOwner
        returns (string memory)
    {
        url = _url;
        emit UrlUpdated(_url);
        return url;
    }

    function updateCashValuationCap(uint256 cashCap)
        public
        onlyOwner
        returns (uint256)
    {
        cashValauationCap = cashCap;
        emit ValuationCapUpdated(cashCap);
        return cashValauationCap;
    }

    function changeOwner(address payable newOwner) external onlyOwner {
        owner = newOwner;
        emit OwnerChanged(newOwner);
    }

    function stockTokenAddress() public view returns (address) {
        return address(stockToken);
    }

    function _preValidateData(address beneficiary, uint256 amount)
        internal
        pure
    {
        require(beneficiary != address(0), "Beneficiary can't be zero address");
        require(amount != 0, "amount can't be 0");
    }

    function contractCashBalance() public view returns (uint256 cashBalance) {
        return cash.balanceOf(address(this));
    }

    function contractStockTokenBalance()
        public
        view
        returns (uint256 stockTokenBalance)
    {
        return stockToken.balanceOf(address(this));
    }

    function stockTokenCashValuation() public view returns (uint256) {
        uint256 cashEquivalent = (
            contractStockTokenBalance().mul(stockToCashRate)
        )
            .div(stockTokenMultiplier);
        return cashEquivalent;
    }

    function contractCashValuation()
        public
        view
        returns (uint256 cashValauation)
    {
        uint256 cashEquivalent = (
            contractStockTokenBalance().mul(stockToCashRate)
        )
            .div(stockTokenMultiplier);
        return contractCashBalance().add(cashEquivalent);
    }

    function updatePoolRate() public returns (uint256 poolrate) {
        if (totalSupply() == 0) {
            poolToCashRate = (10**(cashDecimals)).mul(1);
        } else {
            poolToCashRate = (
                (contractCashValuation().mul(1e18)).div(totalSupply())
            );
        }
        emit PoolRateUpdated(poolrate);
        return poolToCashRate;
    }

    function mintPoolToken(uint256 inputCashAmount) external {
        if (cashValauationCap != 0) {
            require(
                inputCashAmount.add(contractCashValuation()) <=
                    cashValauationCap,
                "inputCashAmount exceeds cashValauationCap"
            );
        }
        address sender = msg.sender;
        _preValidateData(sender, inputCashAmount);
        updatePoolRate();
        uint256 actualCashReceived = doTransferIn(
            address(cash),
            sender,
            inputCashAmount
        );

        // calculate pool token amount to be minted
        uint256 poolTokens = (actualCashReceived.mul(1e18)).div(poolToCashRate);
        _mint(sender, poolTokens); //Minting  Pool Token
        emit PoolTokensMinted(sender, actualCashReceived, poolTokens);
    }

    function burnPoolToken(uint256 poolTokenAmount) external {
        address sender = msg.sender;
        _preValidateData(sender, poolTokenAmount);

        updatePoolRate();
        uint256 cashToRedeem = (
            (poolTokenAmount.mul(poolToCashRate)).div(1e18)
        );
        _burn(sender, poolTokenAmount);

        uint256 outputStockToken = 0;
        uint256 outputCashAmount = 0;

        if (stockTokenCashValuation() >= cashToRedeem) {
            outputStockToken = (
                (cashToRedeem.mul(stockTokenMultiplier)).div(stockToCashRate)
            ); //calculate stock token amount to be return
            doTransferOut(address(stockToken), sender, outputStockToken);
        } else if (cashToRedeem > stockTokenCashValuation()) {
            outputStockToken = contractStockTokenBalance();
            outputCashAmount = cashToRedeem.sub(stockTokenCashValuation()); // calculate cash amount to be return
            doTransferOut(address(stockToken), sender, outputStockToken);
            doTransferOut(address(cash), sender, outputCashAmount);
        }
        emit PoolTokensBurnt(
            sender,
            poolTokenAmount,
            outputStockToken,
            outputCashAmount
        );
    }

    function redeemStockToken(uint256 stockTokenAmount) external {
        address sender = msg.sender;
        _preValidateData(sender, stockTokenAmount);
        uint256 actualStockReceived = doTransferIn(
            address(stockToken),
            sender,
            stockTokenAmount
        );

        // calculate Cash amount to be return
        uint256 outputCashAmount = (actualStockReceived.mul(stockToCashRate))
            .div(stockTokenMultiplier);
        doTransferOut(address(cash), sender, outputCashAmount);
        emit StockTokensRedeemed(sender, actualStockReceived, outputCashAmount);
    }

    /**
     * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case.
     *      This will revert due to insufficient balance or insufficient allowance.
     *      This function returns the actual amount received,
     *      which may be less than `amount` if there is a fee attached to the transfer.
     *
     *      Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value.
     *            See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
     */
    function doTransferIn(
        address tokenAddress,
        address from,
        uint256 amount
    ) internal returns (uint256) {
        INonStandardERC20 token = INonStandardERC20(tokenAddress);
        uint256 balanceBefore = IERC20(tokenAddress).balanceOf(address(this));
        token.transferFrom(from, address(this), amount);

        bool success;
        assembly {
            switch returndatasize()
                case 0 {
                    // This is a non-standard ERC-20
                    success := not(0) // set success to true
                }
                case 32 {
                    // This is a compliant ERC-20
                    returndatacopy(0, 0, 32)
                    success := mload(0) // Set `success = returndata` of external call
                }
                default {
                    // This is an excessively non-compliant ERC-20, revert.
                    revert(0, 0)
                }
        }
        require(success, "TOKEN_TRANSFER_IN_FAILED");

        // Calculate the amount that was *actually* transferred
        uint256 balanceAfter = IERC20(tokenAddress).balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "TOKEN_TRANSFER_IN_OVERFLOW");
        return balanceAfter - balanceBefore; // underflow already checked above, just subtract
    }

    /**
     * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory
     *      error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to
     *      insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified
     *      it is >= amount, this should not revert in normal conditions.
     *
     *      Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value.
     *            See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
     */
    function doTransferOut(
        address tokenAddress,
        address to,
        uint256 amount
    ) internal {
        INonStandardERC20 token = INonStandardERC20(tokenAddress);
        token.transfer(to, amount);

        bool success;
        assembly {
            switch returndatasize()
                case 0 {
                    // This is a non-standard ERC-20
                    success := not(0) // set success to true
                }
                case 32 {
                    // This is a complaint ERC-20
                    returndatacopy(0, 0, 32)
                    success := mload(0) // Set `success = returndata` of external call
                }
                default {
                    // This is an excessively non-compliant ERC-20, revert.
                    revert(0, 0)
                }
        }
        require(success, "TOKEN_TRANSFER_OUT_FAILED");
    }
}
function doTransferOut(
        address tokenAddress,
        address to,
        uint256 amount
    ) internal {
        INonStandardERC20 token = INonStandardERC20(tokenAddress);
        token.transfer(to, amount);

        bool success;
        assembly {
            switch returndatasize()
                case 0 {
                    // This is a non-standard ERC-20
                    success := not(0) // set success to true
                }
                case 32 {
                    // This is a complaint ERC-20
                    returndatacopy(0, 0, 32)
                    success := mload(0) // Set `success = returndata` of external call
                }
                default {
                    // This is an excessively non-compliant ERC-20, revert.
                    revert(0, 0)
                }
        }
        require(success, "TOKEN_TRANSFER_OUT_FAILED");
    }
/**
 * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory
 *      error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to
 *      insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified
 *      it is >= amount, this should not revert in normal conditions.
 *
 *      Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value.
 *            See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
 */