contract HegicOptions is Ownable, IHegicOptions {
    using SafeMath for uint;
    using SafeERC20 for IERC20;

    Option[] public override options;
    uint public impliedVolRate;
    uint public optionCollateralizationRatio = 100;
    uint internal contractCreationTimestamp;
    
    
    uint internal constant IV_DECIMALS = 1e8;
    IUniswapV2SlidingOracle public constant ORACLE = IUniswapV2SlidingOracle(0xCA2E2df6A7a7Cf5bd19D112E8568910a6C2D3885);
    uint8 constant public GRANULARITY = 8;
    
    IERC20 constant public DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
    IERC20 constant public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IERC20 constant public USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
    address constant public WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    
    ICurveFi constant public CURVE = ICurveFi(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7);
    IyVault constant public YEARN = IyVault(0x9cA85572E6A3EbF24dEDd195623F188735A5179f);
    IERC20 constant public CRV3 = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490);
    address immutable public ASSET;
    uint immutable public ONE;
    IHegicERCPool immutable public POOL;
    
    uint public cummulativeStrike;
    uint public cummulativeAmount;
    uint public cummulativeCalls;
    uint public cummulativePuts;
    
    constructor(
        IHegicERCPool _pool,
        address _asset
    ) public {
        POOL = _pool;
        ASSET = _asset;
        ONE = uint(10)**ERC20(_asset).decimals();
        impliedVolRate = 4500;
        contractCreationTimestamp = block.timestamp;
    }

    /**
     * @notice Used for adjusting the options prices while balancing asset's implied volatility rate
     * @param value New IVRate value
     */
    function setImpliedVolRate(uint value) external onlyOwner {
        require(value >= 1000, "ImpliedVolRate limit is too small");
        impliedVolRate = value;
    }

    /**
     * @notice Used for changing option collateralization ratio
     * @param value New optionCollateralizationRatio value
     */
    function setOptionCollaterizationRatio(uint value) external onlyOwner {
        require(50 <= value && value <= 100, "wrong value");
        optionCollateralizationRatio = value;
    }
    
    /**
     * @notice Provides a quote of how much output can be expected given the inputs
     * @param tokenIn the asset being received
     * @param amountIn the amount of tokenIn being provided
     * @return minOut the minimum amount of liquidity to send
     */
    function quote(address tokenIn, uint amountIn) public view returns (uint minOut) {
        if (tokenIn != WETH) {
            amountIn = ORACLE.quote(tokenIn, amountIn, WETH, GRANULARITY);
        }
        minOut = ORACLE.quote(WETH, amountIn, address(DAI), GRANULARITY);
    }
    
    /**
     * @notice Creates a new option
     * @param period Option period in seconds (1 days <= period <= 4 weeks)
     * @param amount Option amount
     * @param strike Strike price of the option
     * @param optionType Call or Put option type
     * @return optionID Created option's ID
     */
    function create(
        address asset,
        uint period,
        uint amount, // amount of the underlying asset (address, amountIn)
        uint strike, // price in DAI as per quote(address, uint)
        uint maxFee,
        OptionType optionType
    ) external returns (uint optionID)  {
        require(
            asset == address(DAI) || asset == address(USDC) || asset == address(USDT), 
            "invalid asset"
        );
        require(
            optionType == OptionType.Call || optionType == OptionType.Put,
            "Wrong option type"
        );
        require(period >= 1 days, "Period is too short");
        require(period <= 4 weeks, "Period is too long");
        
        cummulativeAmount = cummulativeAmount.add(amount);
        cummulativeStrike = cummulativeStrike.add(strike.mul(amount));
        
        uint amountInDAI = quote(ASSET, amount);
        
        if (optionType == OptionType.Call) {
            cummulativeCalls = cummulativeCalls.add(amountInDAI);
        } else  {
            cummulativePuts = cummulativePuts.add(amountInDAI);
        }
        
        (uint total,, uint strikeFee, ) =
            fees(period, amountInDAI, strike, optionType);
        
        uint _fee = convertDAI2Asset(asset, total);
        
        require(amount > strikeFee, "price difference is too large");
        require(_fee < maxFee, "fee exceeds max fee");
        optionID = options.length;
        
        IERC20(asset).safeTransferFrom(msg.sender, address(this), _fee);
        convertToY3P();

        Option memory option = Option(
            State.Active, // state
            POOL.getNextID(), // lockID
            msg.sender, // holder
            strike,
            amount,
            DAI2Y3P(amountInDAI),
            YEARN.balanceOf(address(this)), // premium
            block.timestamp + period, // expiration
            optionType
        );

        
        
        options.push(option);
        POOL.lock(option.lockID, option.lockedAmount, option.premium);

        emit Create(optionID, msg.sender, total);
    }
    
    function convertDAI2Asset(address asset, uint total) public view returns (uint) {
        return total.mul(ERC20(asset).decimals()).div(ERC20(address(DAI)).decimals());
    }

    /**
     * @notice Transfers an active option
     * @param optionID ID of your option
     * @param newHolder Address of new option holder
     */
    function transfer(uint optionID, address payable newHolder) external {
        Option storage option = options[optionID];

        require(newHolder != address(0), "new holder address is zero");
        require(option.expiration >= block.timestamp, "Option has expired");
        require(option.holder == msg.sender, "Wrong msg.sender");
        require(option.state == State.Active, "Only active option could be transferred");

        option.holder = newHolder;
    }

    /**
     * @notice Exercises an active option
     * @param optionID ID of your option
     */
    function exercise(uint optionID) external {
        Option storage option = options[optionID];

        require(option.expiration >= block.timestamp, "Option has expired");
        require(option.holder == msg.sender, "Wrong msg.sender");
        require(option.state == State.Active, "Wrong state");

        option.state = State.Exercised;
        uint profit = _payProfit(optionID);
        
        cummulativeStrike = cummulativeStrike.sub(option.strike);
        cummulativeAmount = cummulativeAmount.sub(option.amount);
        if (option.optionType == OptionType.Call) {
            cummulativeCalls = cummulativeCalls.sub(option.lockedAmount);
        } else if (option.optionType == OptionType.Put) {
            cummulativePuts = cummulativePuts.sub(option.lockedAmount);
        }

        emit Exercise(optionID, profit);
    }

    /**
     * @notice Unlocks an array of options
     * @param optionIDs array of options
     */
    function unlockAll(uint[] calldata optionIDs) external {
        uint arrayLength = optionIDs.length;
        uint _cummulativeStrike;
        uint _cummulativeAmount;
        uint _cummulativeCalls; 
        uint _cummulativePuts;
        
        for (uint i = 0; i < arrayLength; i++) {
            (uint _strike, uint _amount, uint _calls, uint _puts) = _unlock(optionIDs[i]);
            _cummulativeStrike = _cummulativeStrike.add(_strike);
            _cummulativeAmount = _cummulativeAmount.add(_amount);
            _cummulativeCalls = _cummulativeCalls.add(_calls);
            _cummulativePuts = _cummulativePuts.add(_puts);
        }
        
        cummulativeStrike = cummulativeStrike.sub(_cummulativeStrike);
        cummulativeAmount = cummulativeAmount.sub(_cummulativeAmount);
        cummulativeCalls = cummulativeCalls.sub(_cummulativeCalls);
        cummulativePuts = cummulativePuts.sub(_cummulativePuts);
    }

    /**
     * @notice Allows the ERC pool contract to receive and send tokens
     */
    function approve() public {
        IERC20(POOL.RESERVE()).safeApprove(address(POOL), uint(0));
        
        DAI.safeApprove(address(CURVE), uint(0));
        USDT.safeApprove(address(CURVE), uint(0));
        USDC.safeApprove(address(CURVE), uint(0));
        CRV3.safeApprove(address(YEARN), uint(0));
        
        IERC20(POOL.RESERVE()).safeApprove(address(POOL), uint(-1));
        
        DAI.safeApprove(address(CURVE), uint(-1));
        USDT.safeApprove(address(CURVE), uint(-1));
        USDC.safeApprove(address(CURVE), uint(-1));
        CRV3.safeApprove(address(YEARN), uint(-1));
    }

    /**
     * @notice Used for getting the actual options prices
     * @param period Option period in seconds (1 days <= period <= 4 weeks)
     * @param amount Option amount
     * @param strike Strike price of the option
     * @return total Total price to be paid
     * @return settlementFee Amount to be distributed to the HEGIC token holders
     * @return strikeFee Amount that covers the price difference in the ITM options
     * @return periodFee Option period fee amount
     */
    function fees(
        uint period,
        uint amount,
        uint strike,
        OptionType optionType
    )
        public
        view
        returns (
            uint total,
            uint settlementFee,
            uint strikeFee,
            uint periodFee
        )
    {
        uint currentPrice = quote(ASSET, ONE);
        uint _avgStrike = cummulativeStrike == 0 ? currentPrice : cummulativeStrike.div(cummulativeAmount);
        if (optionType == OptionType.Put) {
            currentPrice = currentPrice.add(currentPrice).sub(_avgStrike);
        } else if (optionType == OptionType.Call) {
            currentPrice = currentPrice.add(_avgStrike).sub(currentPrice);
        }
        settlementFee = getSettlementFee(amount);
        periodFee = getPeriodFee(amount, period, strike, currentPrice, optionType);
        strikeFee = getStrikeFee(amount, strike, currentPrice, optionType);
        total = periodFee.add(strikeFee).add(settlementFee);
        if (optionType == OptionType.Put && cummulativePuts > 0) {
            total = total.mul(cummulativePuts).div((cummulativePuts.add(cummulativeCalls)).div(2));
        } else if (cummulativeCalls > 0) {
            total = total.mul(cummulativeCalls).div((cummulativePuts.add(cummulativeCalls)).div(2));
        }
    }

    /**
     * @notice Unlock funds locked in the expired options
     * @param optionID ID of the option
     */
    function unlock(uint optionID) external {
        (uint _strike, uint _amount, uint _calls, uint _puts) = _unlock(optionID);
        cummulativeStrike = cummulativeStrike.sub(_strike);
        cummulativeAmount = cummulativeAmount.sub(_amount);
        cummulativeCalls = cummulativeCalls.sub(_calls);
        cummulativePuts = cummulativePuts.sub(_puts);
    }
    
    function _unlock(uint optionID) internal returns (uint _strike, uint _amount, uint _calls, uint _puts) {
        Option storage option = options[optionID];
        require(option.expiration < block.timestamp, "Option has not expired yet");
        require(option.state == State.Active, "Option is not active");
        option.state = State.Expired;
        POOL.unlock(optionID);
        _strike = _strike.add(option.strike);
        _amount = _amount.add(option.amount);
        if (option.optionType == OptionType.Call) {
            _calls = _calls.add(option.lockedAmount);
        } else if (option.optionType == OptionType.Put) {
            _puts = _puts.add(option.lockedAmount);
        }
        
        emit Expire(optionID, option.premium);
    }
    
    function price() external view returns (uint) {
        return quote(ASSET, ONE);
    }

    /**
     * @notice Calculates settlementFee
     * @param amount Option amount
     * @return fee Settlement fee amount
     */
    function getSettlementFee(uint amount)
        internal
        pure
        returns (uint fee)
    {
        return amount / 100;
    }

    /**
     * @notice Calculates periodFee
     * @param amount Option amount
     * @param period Option period in seconds (1 days <= period <= 4 weeks)
     * @param strike Strike price of the option
     * @param currentPrice Current price of BTC
     * @return fee Period fee amount
     *
     * amount < 1e30        |
     * impliedVolRate < 1e10| => amount * impliedVolRate * strike < 1e60 < 2^uint
     * strike < 1e20 ($1T)  |
     *
     * in case amount * impliedVolRate * strike >= 2^256
     * transaction will be reverted by the SafeMath
     */
    function getPeriodFee(
        uint amount,
        uint period,
        uint strike,
        uint currentPrice,
        OptionType optionType
    ) internal view returns (uint fee) {
        if (optionType == OptionType.Put)
            return amount
                .mul(sqrt(period))
                .mul(impliedVolRate)
                .mul(strike)
                .div(currentPrice)
                .div(IV_DECIMALS);
        else
            return amount
                .mul(sqrt(period))
                .mul(impliedVolRate)
                .mul(currentPrice)
                .div(strike)
                .div(IV_DECIMALS);
    }

    /**
     * @notice Calculates strikeFee
     * @param amount Option amount
     * @param strike Strike price of the option
     * @param currentPrice Current price of BTC
     * @return fee Strike fee amount
     */
    function getStrikeFee(
        uint amount,
        uint strike,
        uint currentPrice,
        OptionType optionType
    ) internal pure returns (uint fee) {
        if (strike > currentPrice && optionType == OptionType.Put)
            return strike.sub(currentPrice).mul(amount).div(currentPrice);
        if (strike < currentPrice && optionType == OptionType.Call)
            return currentPrice.sub(strike).mul(amount).div(currentPrice);
        return 0;
    }

    /**
     * @notice Sends profits in RESERVE from the RESERVE pool to an option holder's address
     * @param optionID A specific option contract id
     */
    function _payProfit(uint optionID)
        internal
        returns (uint profit)
    {
        Option memory option = options[optionID];
        uint currentPrice = quote(ASSET, ONE);
        if (option.optionType == OptionType.Call) {
            require(option.strike <= currentPrice, "Current price is too low");
            profit = currentPrice.sub(option.strike).mul(option.amount);
        } else {
            require(option.strike >= currentPrice, "Current price is too high");
            profit = option.strike.sub(currentPrice).mul(option.amount);
        }
        if (profit > option.lockedAmount)
            profit = option.lockedAmount;
            
        profit = DAI2Y3P(profit);
        POOL.send(option.lockID, option.holder, profit);
    }
    
    function DAI2Y3P(uint amount) public view returns (uint _yearn) {
        uint _curve = amount.mul(1e18).div(CURVE.get_virtual_price());
        _yearn = _curve.mul(1e18).div(YEARN.getPricePerFullShare());
    }
    
    function convertToY3P() internal {
        CURVE.add_liquidity([DAI.balanceOf(address(this)), USDC.balanceOf(address(this)), USDT.balanceOf(address(this))], 0);
        YEARN.depositAll();
    }

    /**
     * @return result Square root of the number
     */
    function sqrt(uint x) private pure returns (uint result) {
        result = x;
        uint k = x.div(2).add(1);
        while (k < result) (result, k) = (k, x.div(k).add(k).div(2));
    }
}
/**
 * @author 0mllwntrmt3 & @andrecronje
 * @title Hegic Generic Bidirectional (Call and Put) Options
 * @notice Hegic Protocol Options Contract
 */
function approve() public {
    IERC20(POOL.RESERVE()).safeApprove(address(POOL), uint(0));
        
    DAI.safeApprove(address(CURVE), uint(0));
    USDT.safeApprove(address(CURVE), uint(0));
    USDC.safeApprove(address(CURVE), uint(0));
    CRV3.safeApprove(address(YEARN), uint(0));
        
    IERC20(POOL.RESERVE()).safeApprove(address(POOL), uint(-1));
        
    DAI.safeApprove(address(CURVE), uint(-1));
    USDT.safeApprove(address(CURVE), uint(-1));
    USDC.safeApprove(address(CURVE), uint(-1));
    CRV3.safeApprove(address(YEARN), uint(-1));
}

/**
 * @notice Allows the ERC pool contract to receive and send tokens
 */