// File: contracts/AMM.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./lib/AMMLib.sol";
import "./PriceOracle.sol";
import "./OptionVault.sol";
import "./interfaces/IOptionVault.sol";
import "./interfaces/IFeePool.sol";
import "./interfaces/IAMM.sol";

/**
 * @notice AMM contract is Automated Market Maker for option contracts.
 * It manages LP(Liquidity provider) tokens and liquidity to write options.
 */
contract AMM is IAMM, ERC1155, Ownable, IERC1155Receiver, ReentrancyGuard {
    using AMMLib for AMMLib.PoolInfo;
    using AMMLib for mapping(uint32 => AMMLib.Tick);
    using AMMLib for AMMLib.Tick;

    /// @dev pool info
    AMMLib.PoolInfo poolInfo;

    /// @dev price oracle contract
    PriceOracle priceOracle;

    /// @dev fee pool contract
    IFeePool public override feePool;

    /// @dev operator address
    address operator;

    /// @dev bot address
    address bot;

    /// @dev emergency mode or not
    bool isEmergencyMode;

    /// @dev nobody can provide liquidity after depositAllowedUntil timestamp
    uint256 depositAllowedUntil;

    /// @dev lp address => rangeId => amount of reservation
    mapping(address => mapping(uint256 => AMMLib.Reservation)) public reservations;

    /// @dev last provided timestamp of liquidity
    mapping(address => uint256) public lastProvidedAt;

    /// @dev lockup period for liquidity withdrawal
    uint256 lockupPeriod = 2 weeks;

    mapping(address => bool) addressesAllowedSkippingLockup;

    // events
    event Deposited(address indexed account, address asset, uint256 rangeId, uint128 amount, uint128 mint);
    event Withdrawn(address indexed account, address asset, uint256 rangeId, uint128 amount, uint128 burn);
    event OptionBought(uint256 seriesId, address indexed buyer, uint128 amount, uint128 premium);
    event OptionSold(uint256 seriesId, address indexed seller, uint128 amount, uint128 premium);
    event Settled(uint256 indexed _expiryId, uint128 protocolFee);
    event EmergencyStateChanged(bool isEmergencyMode);
    event ConfigUpdated(uint8 key, uint128 value);
    event DepositAllowedUntilUpdated(uint256 depositAllowedUntil);
    event LockupPeriodUpdated(uint256 period);

    modifier onlyOperator() {
        require(msg.sender == operator, "AMM: caller must be operator");
        _;
    }

    modifier onlyBot() {
        require(msg.sender == bot, "AMM: caller must be bot");
        _;
    }

    modifier isDepositAllowed() {
        require(block.timestamp < depositAllowedUntil, "AMM: deposit not allowed");
        _;
    }

    modifier notEmergencyMode() {
        require(!isEmergencyMode, "AMM: emergency mode");
        _;
    }

    constructor(
        string memory _uri,
        address _aggregator,
        address _collateral,
        address _priceOracle,
        address _feeRecipient,
        address _operator,
        address _optionContract
    ) ERC1155(_uri) {
        operator = _operator;
        bot = _operator;

        priceOracle = PriceOracle(_priceOracle);

        feePool = IFeePool(_feeRecipient);

        depositAllowedUntil = 2**256 - 1;

        poolInfo.init(_aggregator, _collateral, _optionContract);
    }

    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes memory data
    ) external override(IERC1155Receiver) returns (bytes4) {
        return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
    }

    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) external override(IERC1155Receiver) returns (bytes4) {
        return bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"));
    }

    function _beforeTokenTransfer(
        address _operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual override(ERC1155) {
        super._beforeTokenTransfer(_operator, from, to, ids, amounts, data);

        if (!addressesAllowedSkippingLockup[_operator]) {
            // LPs can't transfer LP tokens before lockupPeriod
            require(lastProvidedAt[from] + lockupPeriod <= block.timestamp, "AMM: liquidity is locked up");
        }
    }

    /**
     * @notice deposit collateral asset to pool
     * price of LP token is '(balance - lockedPremium) / supply'
     * @param _mintAmount amount of write token to mint.
     *   it must be muptiples of _tickEnd - _tickStart.
     * @param _maxDeposit max deposit collateral amount scaled by 1e6
     * @param _tickStart lower tick
     * @param _tickEnd upper tick
     */
    function deposit(
        uint128 _mintAmount,
        uint128 _maxDeposit,
        uint32 _tickStart,
        uint32 _tickEnd
    ) external notEmergencyMode isDepositAllowed nonReentrant {
        // validate inputs
        AMMLib.validateRange(_tickStart, _tickEnd);

        // mint LP tokens
        uint128 amountDeposited = poolInfo.addBalance(_tickStart, _tickEnd, _mintAmount);
        require(amountDeposited > 0, "AMM: amount is too small");
        require(amountDeposited <= _maxDeposit, "AMM: amount deposited is greater than max");
        uint128 rangeId = genRangeId(_tickStart, _tickEnd);

        // receive collateral from LP
        IERC20(poolInfo.collateral).transferFrom(msg.sender, address(this), amountDeposited);

        // mint LP tokens
        _mint(msg.sender, rangeId, _mintAmount, "");

        lastProvidedAt[msg.sender] = block.timestamp;

        //emit event
        emit Deposited(msg.sender, poolInfo.collateral, rangeId, amountDeposited, _mintAmount);
    }

    /**
     * @notice reserve withdrawal.
     * LPs can make reservation for withdrawal.
     * withdrawable timestamp is last expiry of live option serieses.
     * The AMM sets aside funds for withdrawals at each maturity.
     * @param _reserveAmount amount of LP token
     * @param _rangeId range id represents lower tick to upper tick
     */
    function reserveWithdrawal(uint128 _reserveAmount, uint128 _rangeId) external {
        // validate inputs
        require(_reserveAmount > 0, "AMM: amount is too small");

        (uint32 tickStart, uint32 tickEnd) = getRange(_rangeId);
        AMMLib.validateRange(tickStart, tickEnd);
        AMMLib.Reservation storage reservation = reservations[msg.sender][_rangeId];

        require(balanceOf(msg.sender, _rangeId) >= _reserveAmount + reservation.burn, "AMM: amount is too large");

        // get the last expiry
        uint128 withdrawableTimestamp = poolInfo.optionVault.getLastExpiry();

        poolInfo.reserveWithdrawal(tickStart, tickEnd, _reserveAmount);

        if (reservation.burn > 0) {
            reservation.burn += _reserveAmount;
            reservation.withdrawableTimestamp = withdrawableTimestamp;
        } else {
            reservations[msg.sender][_rangeId] = AMMLib.Reservation(_reserveAmount, withdrawableTimestamp);
        }
    }

    /**
     * @notice withdraw collateral asset from pool
     * withdrawable amount is calculated by the following formula.
     * amount = (burn amount) * (pool quote value) / supply
     * However, if available amount is less than the withdrawable amount,
     * LP need to make reservation in advance.
     * LPs should specify minimum withdrawable amount as _minWithdrawal
     * @param _burnAmount amount of write token to burn.
     * @param _minWithdrawal minimal withdraw collateral amount scaled by 1e6
     * @param _rangeId range id represents lower tick to upper tick
     * @param _useReservation use reservation or not
     */
    function withdraw(
        uint128 _burnAmount,
        uint128 _minWithdrawal,
        uint128 _rangeId,
        bool _useReservation
    ) external notEmergencyMode nonReentrant {
        // validate inputs
        (uint32 tickStart, uint32 tickEnd) = getRange(_rangeId);
        AMMLib.Reservation memory reservation = reservations[msg.sender][_rangeId];

        AMMLib.validateRange(tickStart, tickEnd);

        require(balanceOf(msg.sender, _rangeId) >= _burnAmount, "AMM: msg.sender doesn't have enough LP tokens");

        // burn LP tokens
        uint128 withdrawnAmount;

        if (_useReservation) {
            require(_burnAmount <= reservation.burn, "AMM: burnAmount must be reserved");

            require(block.timestamp > reservation.withdrawableTimestamp, "AMM: withdrawable period must have passed");

            withdrawnAmount = poolInfo.removeBalanceFromReservation(tickStart, tickEnd, _burnAmount);

            reservations[msg.sender][_rangeId].burn -= _burnAmount;
        } else {
            require(reservation.burn == 0, "AMM: reservation must not be exists");

            withdrawnAmount = poolInfo.removeBalance(tickStart, tickEnd, _burnAmount);
        }

        require(withdrawnAmount > 0, "AMM: amount is too small");

        _burn(msg.sender, _rangeId, _burnAmount);

        // send collateral to LP
        require(withdrawnAmount >= _minWithdrawal, "AMM: _burnAmount is too small");
        IERC20(poolInfo.collateral).transfer(msg.sender, withdrawnAmount);

        //emit event
        emit Withdrawn(msg.sender, poolInfo.collateral, _rangeId, withdrawnAmount, _burnAmount);
    }

    /**
     * @notice calculate option premium
     * @param _seriesId option series id
     * @param _size option size scaled by 1e8
     */
    function calculatePremium(
        uint256 _seriesId,
        uint128 _size,
        bool _isSelling
    ) external view returns (uint128) {
        require(_size > 0, "AMM: size must not be 0");

        uint128 spot = getPrice();

        return poolInfo.calculatePremium(_seriesId, _size, spot, _isSelling);
    }

    /**
     * @notice send premium and receive options
     * @param _seriesId option series id
     * @param _amount amount to buy scaled by 1e8
     * @param _maxFee max total amount of premium to pay
     */
    function buy(
        uint256 _seriesId,
        uint128 _amount,
        uint128 _maxFee
    ) external notEmergencyMode nonReentrant {
        require(_amount > 0, "AMM: amount must not be 0");

        uint128 spot = getPrice();

        uint128 premium = poolInfo.buy(_seriesId, _amount, spot, msg.sender);
        require(premium <= _maxFee, "AMM: total fee exceeds maxFeeAmount");

        // receive premium from trader
        IERC20(poolInfo.collateral).transferFrom(msg.sender, address(this), premium);

        emit OptionBought(_seriesId, msg.sender, _amount, premium);
    }

    /**
     * @notice send options and receive premium
     * @param _seriesId option series id
     * @param _amount amount to sell scaled by 1e8
     * @param _minFee minimal premium to receive
     */
    function sell(
        uint256 _seriesId,
        uint128 _amount,
        uint128 _minFee
    ) external override notEmergencyMode nonReentrant returns (uint128) {
        require(_amount > 0, "AMM: amount must not be 0");

        require(
            ERC1155(address(poolInfo.optionVault)).balanceOf(msg.sender, _seriesId) >= _amount,
            "AMM: msg.sender doesn't have enough amount"
        );

        ERC1155(address(poolInfo.optionVault)).safeTransferFrom(msg.sender, address(this), _seriesId, _amount, "");

        uint128 spot = getPrice();

        uint128 premium = poolInfo.sell(_seriesId, _amount, spot, msg.sender);

        require(premium >= _minFee, "AMM: premium is too low");

        // send premium to trader from pool
        IERC20(poolInfo.collateral).transfer(msg.sender, premium);

        // emit event
        emit OptionSold(_seriesId, msg.sender, _amount, premium);

        return premium;
    }

    ////////////////////////
    // Operator Functions //
    ////////////////////////

    /**
     * @notice settle option serieses of an expiration
     * settle vaults for pool's short positions and unlock collaterals.
     * claim profit of pool's long positions.
     * @param _expiryId expiration id
     */
    function settle(uint256 _expiryId) external onlyBot {
        uint128 protocolFee = poolInfo.settle(_expiryId);

        if (protocolFee > 0) {
            // send protocolFee to fee recipient
            IERC20(poolInfo.collateral).approve(address(feePool), protocolFee);
            feePool.sendProfitERC20(address(this), protocolFee);
        }

        emit Settled(_expiryId, protocolFee);
    }

    /**
     * @notice rebalance collateral for a tick
     * withdraw collaterals from the vault if there are extra collaterals.
     * Only the operator can call this function.
     * @param _tickId tick id
     * @param _expiryId expiry id
     */
    function rebalanceCollateral(uint32 _tickId, uint256 _expiryId) external onlyOperator {
        poolInfo.rebalanceCollateral(_tickId, _expiryId);
    }

    /**
     * @notice set or unset emergency mode
     * @param _isEmergencyMode if true, set emergency mode.
     *  if false unset emergency mode.
     */
    function changeState(bool _isEmergencyMode) external onlyOperator {
        isEmergencyMode = _isEmergencyMode;

        emit EmergencyStateChanged(_isEmergencyMode);
    }

    /**
     * @notice set depositAllowedUntil parameter
     * @param _depositAllowedUntil no one can provide liquidity after this timestamp
     */
    function setDepositAllowedUntil(uint256 _depositAllowedUntil) external onlyOperator {
        depositAllowedUntil = _depositAllowedUntil;

        emit DepositAllowedUntilUpdated(_depositAllowedUntil);
    }

    /**
     * @notice set lockup period
     * @param _lockupPeriod depositor cannot withdraw during lockup period
     */
    function setLockupPeriod(uint256 _lockupPeriod) external onlyOperator {
        lockupPeriod = _lockupPeriod;

        emit LockupPeriodUpdated(_lockupPeriod);
    }

    function setAddressAllowedSkippingLockup(address _address, bool _isAllowed) external onlyOperator {
        addressesAllowedSkippingLockup[_address] = _isAllowed;
    }

    /**
     * @notice update a config value
     */
    function setConfig(uint8 _key, uint128 _value) external onlyOperator {
        poolInfo.configs[_key] = _value;

        // emit event
        emit ConfigUpdated(_key, _value);
    }

    /**
     * @notice set bot address
     * @param _bot bot address
     */
    function setBot(address _bot) external onlyOperator {
        bot = _bot;
    }

    /**
     * @notice set new fee recipient
     * @param _feeRecipient fee recipient
     */
    function setFeeRecipient(address _feeRecipient) external onlyOperator {
        feePool = IFeePool(_feeRecipient);
    }

    /**
     * @notice set new operator
     * @param _operator operator address
     */
    function setNewOperator(address _operator) external onlyOperator {
        operator = _operator;
    }

    //////////////////////
    // Getter Functions //
    //////////////////////

    /**
     * @notice get mint amount of LP token
     * @param _depositAmount amount of collateral to deposit scaled by 1e6
     * @param _tickStart lower tick
     * @param _tickEnd upper tick
     */
    function getMintAmount(
        uint128 _depositAmount,
        uint32 _tickStart,
        uint32 _tickEnd
    ) external view returns (uint128) {
        return poolInfo.getMintAmount(_tickStart, _tickEnd, _depositAmount);
    }

    /**
     * @notice get withdrawable amount of collateral scaled by 1e6
     * @param _burnAmount LP token to burn
     * @param _tickStart lower tick
     * @param _tickEnd upper tick
     */
    function getWithdrawableAmount(
        uint128 _burnAmount,
        uint32 _tickStart,
        uint32 _tickEnd
    ) external view returns (uint128) {
        return poolInfo.getWithdrawableAmount(_tickStart, _tickEnd, _burnAmount);
    }

    /**
     * @notice get tick list
     * @param _tickStart lower tick
     * @param _tickEnd upper tick
     * @return tick list
     */
    function getTicks(uint32 _tickStart, uint32 _tickEnd) public view returns (AMMLib.Tick[] memory) {
        AMMLib.validateRange(_tickStart, _tickEnd);
        AMMLib.Tick[] memory _ticks = new AMMLib.Tick[](_tickEnd - _tickStart);
        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            _ticks[i - _tickStart] = poolInfo.ticks[i];
        }
        return _ticks;
    }

    function getSeriesState(uint32 _tickId, uint256 _seriesId)
        public
        view
        returns (AMMLib.LockedOptionStatePerTick memory, bool exists)
    {
        return AMMLib.getLockedOptionStatePerTick(poolInfo, _seriesId, _tickId);
    }

    function getProfitState(uint32 _tickId, uint256 _expiryId) public view returns (AMMLib.Profit memory) {
        return poolInfo.profits[_expiryId][_tickId];
    }

    /**
     * @notice get tick cumulative of earning seconds per liquidity
     */
    function getSecondsPerLiquidity(uint32 _tickLower, uint32 _tickUpper) external view returns (uint128) {
        AMMLib.validateRange(_tickLower, _tickUpper);
        return poolInfo.ticks.getSecondsPerLiquidity(_tickLower, _tickUpper);
    }

    function getLockupPeriod() external view returns (uint256) {
        return lockupPeriod;
    }

    /**
     * @notice get a config value
     */
    function getConfig(uint8 _key) external view returns (uint128) {
        return poolInfo.configs[_key];
    }

    function genRangeId(uint32 _tickStart, uint32 _tickEnd) public pure returns (uint128) {
        return _tickStart + 1e2 * _tickEnd;
    }

    function getRange(uint128 _rangeId) public pure returns (uint32 _start, uint32 _end) {
        _start = uint32(_rangeId % 1e2);
        _end = uint32(_rangeId / 1e2);
    }

    function getPrice() internal view returns (uint128) {
        (uint256 spot, ) = priceOracle.getPrice(poolInfo.aggregator);
        return uint128(spot);
    }
}

// File: @openzeppelin/contracts/token/ERC1155/ERC1155.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./extensions/IERC1155MetadataURI.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/introspection/ERC165.sol";

/**
 * @dev Implementation of the basic standard multi-token.
 * See https://eips.ethereum.org/EIPS/eip-1155
 * Originally based on code by Enjin: https://github.com/enjin/erc-1155
 *
 * _Available since v3.1._
 */
contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
    using Address for address;

    // Mapping from token ID to account balances
    mapping (uint256 => mapping(address => uint256)) private _balances;

    // Mapping from account to operator approvals
    mapping (address => mapping(address => bool)) private _operatorApprovals;

    // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json
    string private _uri;

    /**
     * @dev See {_setURI}.
     */
    constructor (string memory uri_) {
        _setURI(uri_);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return interfaceId == type(IERC1155).interfaceId
            || interfaceId == type(IERC1155MetadataURI).interfaceId
            || super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC1155MetadataURI-uri}.
     *
     * This implementation returns the same URI for *all* token types. It relies
     * on the token type ID substitution mechanism
     * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP].
     *
     * Clients calling this function must replace the `\{id\}` substring with the
     * actual token type ID.
     */
    function uri(uint256) public view virtual override returns (string memory) {
        return _uri;
    }

    /**
     * @dev See {IERC1155-balanceOf}.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
        require(account != address(0), "ERC1155: balance query for the zero address");
        return _balances[id][account];
    }

    /**
     * @dev See {IERC1155-balanceOfBatch}.
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     */
    function balanceOfBatch(
        address[] memory accounts,
        uint256[] memory ids
    )
        public
        view
        virtual
        override
        returns (uint256[] memory)
    {
        require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");

        uint256[] memory batchBalances = new uint256[](accounts.length);

        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }

        return batchBalances;
    }

    /**
     * @dev See {IERC1155-setApprovalForAll}.
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(_msgSender() != operator, "ERC1155: setting approval status for self");

        _operatorApprovals[_msgSender()][operator] = approved;
        emit ApprovalForAll(_msgSender(), operator, approved);
    }

    /**
     * @dev See {IERC1155-isApprovedForAll}.
     */
    function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[account][operator];
    }

    /**
     * @dev See {IERC1155-safeTransferFrom}.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    )
        public
        virtual
        override
    {
        require(to != address(0), "ERC1155: transfer to the zero address");
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: caller is not owner nor approved"
        );

        address operator = _msgSender();

        _beforeTokenTransfer(operator, from, to, _asSingletonArray(id), _asSingletonArray(amount), data);

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        _balances[id][from] = fromBalance - amount;
        _balances[id][to] += amount;

        emit TransferSingle(operator, from, to, id, amount);

        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
    }

    /**
     * @dev See {IERC1155-safeBatchTransferFrom}.
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    )
        public
        virtual
        override
    {
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
        require(to != address(0), "ERC1155: transfer to the zero address");
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: transfer caller is not owner nor approved"
        );

        address operator = _msgSender();

        _beforeTokenTransfer(operator, from, to, ids, amounts, data);

        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
            _balances[id][from] = fromBalance - amount;
            _balances[id][to] += amount;
        }

        emit TransferBatch(operator, from, to, ids, amounts);

        _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
    }

    /**
     * @dev Sets a new URI for all token types, by relying on the token type ID
     * substitution mechanism
     * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP].
     *
     * By this mechanism, any occurrence of the `\{id\}` substring in either the
     * URI or any of the amounts in the JSON file at said URI will be replaced by
     * clients with the token type ID.
     *
     * For example, the `https://token-cdn-domain/\{id\}.json` URI would be
     * interpreted by clients as
     * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json`
     * for token type ID 0x4cce0.
     *
     * See {uri}.
     *
     * Because these URIs cannot be meaningfully represented by the {URI} event,
     * this function emits no events.
     */
    function _setURI(string memory newuri) internal virtual {
        _uri = newuri;
    }

    /**
     * @dev Creates `amount` tokens of token type `id`, and assigns them to `account`.
     *
     * Emits a {TransferSingle} event.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - If `account` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
     * acceptance magic value.
     */
    function _mint(address account, uint256 id, uint256 amount, bytes memory data) internal virtual {
        require(account != address(0), "ERC1155: mint to the zero address");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, address(0), account, _asSingletonArray(id), _asSingletonArray(amount), data);

        _balances[id][account] += amount;
        emit TransferSingle(operator, address(0), account, id, amount);

        _doSafeTransferAcceptanceCheck(operator, address(0), account, id, amount, data);
    }

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}.
     *
     * Requirements:
     *
     * - `ids` and `amounts` must have the same length.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
     * acceptance magic value.
     */
    function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

        for (uint i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }

        emit TransferBatch(operator, address(0), to, ids, amounts);

        _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
    }

    /**
     * @dev Destroys `amount` tokens of token type `id` from `account`
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens of token type `id`.
     */
    function _burn(address account, uint256 id, uint256 amount) internal virtual {
        require(account != address(0), "ERC1155: burn from the zero address");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, account, address(0), _asSingletonArray(id), _asSingletonArray(amount), "");

        uint256 accountBalance = _balances[id][account];
        require(accountBalance >= amount, "ERC1155: burn amount exceeds balance");
        _balances[id][account] = accountBalance - amount;

        emit TransferSingle(operator, account, address(0), id, amount);
    }

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}.
     *
     * Requirements:
     *
     * - `ids` and `amounts` must have the same length.
     */
    function _burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) internal virtual {
        require(account != address(0), "ERC1155: burn from the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, account, address(0), ids, amounts, "");

        for (uint i = 0; i < ids.length; i++) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 accountBalance = _balances[id][account];
            require(accountBalance >= amount, "ERC1155: burn amount exceeds balance");
            _balances[id][account] = accountBalance - amount;
        }

        emit TransferBatch(operator, account, address(0), ids, amounts);
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning, as well as batched variants.
     *
     * The same hook is called on both single and batched variants. For single
     * transfers, the length of the `id` and `amount` arrays will be 1.
     *
     * Calling conditions (for each `id` and `amount` pair):
     *
     * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * of token type `id` will be  transferred to `to`.
     * - When `from` is zero, `amount` tokens of token type `id` will be minted
     * for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens of token type `id`
     * will be burned.
     * - `from` and `to` are never both zero.
     * - `ids` and `amounts` have the same, non-zero length.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    )
        internal
        virtual
    { }

    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    )
        private
    {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                if (response != IERC1155Receiver(to).onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non ERC1155Receiver implementer");
            }
        }
    }

    function _doSafeBatchTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    )
        private
    {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (bytes4 response) {
                if (response != IERC1155Receiver(to).onERC1155BatchReceived.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non ERC1155Receiver implementer");
            }
        }
    }

    function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) {
        uint256[] memory array = new uint256[](1);
        array[0] = element;

        return array;
    }
}

// File: @openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev _Available since v3.1._
 */
interface IERC1155Receiver is IERC165 {

    /**
        @dev Handles the receipt of a single ERC1155 token type. This function is
        called at the end of a `safeTransferFrom` after the balance has been updated.
        To accept the transfer, this must return
        `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
        (i.e. 0xf23a6e61, or its own function selector).
        @param operator The address which initiated the transfer (i.e. msg.sender)
        @param from The address which previously owned the token
        @param id The ID of the token being transferred
        @param value The amount of tokens being transferred
        @param data Additional data with no specified format
        @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
    */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    )
        external
        returns(bytes4);

    /**
        @dev Handles the receipt of a multiple ERC1155 token types. This function
        is called at the end of a `safeBatchTransferFrom` after the balances have
        been updated. To accept the transfer(s), this must return
        `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
        (i.e. 0xbc197c81, or its own function selector).
        @param operator The address which initiated the batch transfer (i.e. msg.sender)
        @param from The address which previously owned the token
        @param ids An array containing ids of each token being transferred (order and length must match values array)
        @param values An array containing amounts of each token being transferred (order and length must match ids array)
        @param data Additional data with no specified format
        @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
    */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    )
        external
        returns(bytes4);
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT

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 () {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

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

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        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 {
        emit OwnershipTransferred(_owner, address(0));
        _owner = 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");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor () {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and make it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        // On the first call to nonReentrant, _notEntered will be true
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;

        _;

        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }
}

// File: contracts/lib/AMMLib.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "../interfaces/IHedge.sol";
import "../interfaces/IOptionVault.sol";
import "./PredyMath.sol";
import "./OptionLib.sol";

/**
 * @title AMMLib
 * @notice The library for option automated market maker
 */
library AMMLib {
    using AMMLib for AMMLib.PoolInfo;
    using AMMLib for AMMLib.Tick;
    using AMMLib for AMMLib.LockedOptionStatePerTick[];
    using PredyMath for uint128;

    /// @dev max uint256
    uint256 constant MAX_UINT256 = 2**256 - 1;

    /// @dev minimum value of tick
    uint32 constant MIN_TICK = 2;

    /// @dev maximum value of tick
    uint32 constant MAX_TICK = 30;

    /// @dev the maximum interval of trading at which the protocol recalculates the premium.
    uint256 constant SAFETY_PERIOD = 6 minutes;

    /**
     * @notice tick is a section of IV
     *   Tick has information about the status of the funds
     * @param supply amount of LP token issued
     * @param balance amount of fund for buying and selling options
     * @param lastSupply last snapshot of supply
     * @param lastBalance last snapshot of balance
     * @param secondsPerLiquidity seconds / liquidity
     * @param reservationForWithdrawal reservation for withdrawal
     * @param hedgePosition current position of the hedge
     */
    struct Tick {
        uint128 supply;
        uint128 balance;
        int128 unrealizedPnL;
        uint128 lastSupply;
        uint128 lastBalance;
        uint128 secondsPerLiquidity;
        uint128 burnReserved;
        uint128 reservationForWithdrawal;
    }

    /**
     * @notice tick state for a option series
     */
    struct LockedOptionStatePerTick {
        // tick id
        uint32 tickId;
        //
        bool isLong;
        // the price per size in last trade recorded per series.
        // scaled by 1e12
        uint128 lastPricePerSize;
        // the timestamp of last trade recorded per series.
        uint128 tradeTime;
    }

    struct Profit {
        // unrealized profit and loss
        int128 unrealizedPnL;
        // cumulative locked premium
        uint128 cumulativeFee;
    }

    /// @dev pool information
    struct PoolInfo {
        address aggregator;
        // option vault contract
        IOptionVault optionVault;
        // collateral address
        address collateral;
        // timestamp of last trade
        uint64 lastBoughtTimestamp;
        // ticks
        mapping(uint32 => Tick) ticks;
        // extra locked amount for each option series
        // tickId => seriesId => buy or sell
        mapping(uint32 => mapping(uint256 => uint128[2])) lockedAmounts;
        // seriesId => LockedOptionState
        mapping(uint256 => LockedOptionStatePerTick[]) locked;
        // expiryId => tickId => Profit
        mapping(uint256 => mapping(uint32 => Profit)) profits;
        // pool configs
        mapping(uint8 => uint128) configs;
    }

    /// @dev buy and sell trade state
    struct TradeState {
        uint32 currentTick;
        uint32 nextTick;
        uint128 stepAmount;
        uint128 remain;
        uint128 currentIV;
        address trader;
        bool isTickLong;
        uint128 pricePerSize;
    }

    /// @dev reservation for withdrawal
    struct Reservation {
        uint128 burn;
        uint128 withdrawableTimestamp;
    }

    uint8 public constant PROTOCOL_FEE_RATIO = 1;
    uint8 public constant MIN_DELTA = 3;
    uint8 public constant BASE_SPREAD = 4;

    /**
     * @notice initialize PoolInfo
     */
    function init(
        PoolInfo storage _pool,
        address _aggregator,
        address _collateral,
        address _optionVault
    ) external {
        require(address(_pool.optionVault) == address(0), "AMMLib: ");

        _pool.collateral = _collateral;
        require(ERC20(_collateral).decimals() == 6);
        _pool.aggregator = _aggregator;
        _pool.optionVault = IOptionVault(_optionVault);

        _pool.configs[PROTOCOL_FEE_RATIO] = 10;
        _pool.configs[MIN_DELTA] = 5 * 1e6;
        // 1 / 500 = 0.2%
        _pool.configs[BASE_SPREAD] = 500;

        IERC20(_pool.collateral).approve(address(_pool.optionVault), MAX_UINT256);
        IERC1155(address(_pool.optionVault)).setApprovalForAll(address(_pool.optionVault), true);
    }

    /**
     * @notice add balance to range
     */
    function addBalance(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _mint
    ) external returns (uint128 depositedAmount) {
        require(_mint % (_tickEnd - _tickStart) == 0, "PoolLib: mint is not multiples of range length");
        uint128 mintPerRange = _mint / (_tickEnd - _tickStart);

        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick storage tick = _pool.ticks[i];
            if (tick.supply > 0) {
                // We use roundUp here to make sure that the money going into the contract
                // is always equal or greater than the money going out.
                uint128 a = calSwapAmountForLPToken(tick, mintPerRange, true);

                tick.balance += a;
                depositedAmount += a;
            } else {
                tick.balance += mintPerRange;
                depositedAmount += mintPerRange;
            }
            tick.supply += mintPerRange;
        }
    }

    /**
     * @notice reserve withdrawal
     */
    function reserveWithdrawal(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _burn
    ) external {
        require(_burn % (_tickEnd - _tickStart) == 0, "AMMLib: burn is not multiples of range length");
        uint128 burnPerRange = _burn / (_tickEnd - _tickStart);

        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick storage tick = _pool.ticks[i];

            tick.burnReserved += burnPerRange;
        }
    }

    /**
     * @notice remove balance from range
     */
    function removeBalance(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _burn
    ) external returns (uint128 withdrawnAmount) {
        require(_burn % (_tickEnd - _tickStart) == 0, "PoolLib: burn is not multiples of range length");
        uint128 burnPerRange = _burn / (_tickEnd - _tickStart);

        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick storage tick = _pool.ticks[i];

            uint128 substraction = removeBalancePerTick(tick, burnPerRange, tick.reservationForWithdrawal);

            withdrawnAmount += substraction;
        }
    }

    /**
     * @notice remove balance from reserved balance
     */
    function removeBalanceFromReservation(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _burn
    ) external returns (uint128 withdrawnAmount) {
        require(_burn % (_tickEnd - _tickStart) == 0, "PoolLib: burn is not multiples of range length");
        uint128 burnPerRange = _burn / (_tickEnd - _tickStart);

        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick storage tick = _pool.ticks[i];

            uint128 subWithdrawReserved = calSwapAmountForLPToken(tick, _burn, false);
            uint128 substraction = removeBalancePerTick(tick, burnPerRange, 0);

            // decrease burn reserved
            tick.burnReserved -= burnPerRange;
            if (tick.reservationForWithdrawal > subWithdrawReserved) {
                tick.reservationForWithdrawal -= subWithdrawReserved;
            } else {
                tick.reservationForWithdrawal = 0;
            }

            withdrawnAmount += substraction;
        }
    }

    /**
     * @notice get premium
     * @param _seriesId option id
     * @param _amount amount to buy scaled by 1e8
     * @return premium total premium
     */
    function calculatePremium(
        PoolInfo storage _pool,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _spot,
        bool _isSelling
    ) external view returns (uint128 premium) {
        IOptionVault.OptionSeriesView memory optionSeries = _pool.optionVault.getOptionSeries(_seriesId);

        TradeState memory step = initializeTradeState(_amount, address(0), optionSeries.iv);

        // calculate premium
        premium = _calculatePremium(_pool, step, optionSeries, _spot, _isSelling);
    }

    /**
     * @notice buy options
     * @param _seriesId option id
     * @param _amount amount to buy scaled by 1e8
     * @return premium total premium
     */
    function buy(
        PoolInfo storage _pool,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _spot,
        address _recipient
    ) external returns (uint128 premium) {
        IOptionVault.OptionSeriesView memory optionSeries = _pool.optionVault.getOptionSeries(_seriesId);

        TradeState memory step = initializeTradeState(_amount, _recipient, optionSeries.iv);

        // process buying
        (premium, step) = _buy(_pool, step, optionSeries, _spot);

        // update IV
        setIV(_pool, _seriesId, step.currentIV);

        _pool.lastBoughtTimestamp = uint64(block.timestamp);
    }

    /**
     * @notice sell options
     * @param _seriesId option id
     * @param _amount amount to sell scaled by 1e8
     * @return premium total premium
     */
    function sell(
        PoolInfo storage _pool,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _spot,
        address _seller
    ) external returns (uint128 premium) {
        IOptionVault.OptionSeriesView memory optionSeries = _pool.optionVault.getOptionSeries(_seriesId);

        require(optionSeries.maturity >= 1 days, "AMMLib: maturity must be greater than 1 days");

        TradeState memory step = initializeTradeState(_amount, _seller, optionSeries.iv);

        // process selling
        (premium, step) = _pool._sell(step, optionSeries, _spot);

        // update IV
        _pool.setIV(_seriesId, step.currentIV);

        _pool.lastBoughtTimestamp = uint64(block.timestamp);
    }

    /**
     * @notice settle option serieses of an expiration
     * unlock all collateral in vaults and calculate unrealized profit and loss.
     */
    function settle(PoolInfo storage _pool, uint256 _expiryId) external returns (uint128 totalProtocolFee) {
        IOptionVault.Expiration memory expiration = _pool.optionVault.getExpiration(_expiryId);

        uint32 minTickId = MAX_TICK;
        uint32 maxTickId = MIN_TICK;

        for (uint256 i = 0; i < expiration.seriesIds.length; i++) {
            (uint32 _minTickId, uint32 _maxTickId) = settleInternal(_pool, expiration.seriesIds[i]);

            if (minTickId > _minTickId) {
                minTickId = _minTickId;
            }

            if (maxTickId < _maxTickId) {
                maxTickId = _maxTickId;
            }
        }

        require(minTickId <= maxTickId, "AMMLib: ticks are already settled");

        for (uint32 i = minTickId; i <= maxTickId; i++) {
            Tick storage tick = _pool.ticks[i];

            totalProtocolFee += settleExpiryInternal(_pool, _expiryId, i, tick);

            tick.balance += _pool.optionVault.settleVault(i, _expiryId);

            // update LP token price
            tick.lastBalance = getPoolValueQuote(_pool, tick, i);
            tick.lastSupply = tick.supply;

            // calculate withdrawal reserved amount
            tick.reservationForWithdrawal = PredyMath.mulDiv(
                tick.burnReserved,
                tick.lastBalance,
                tick.lastSupply,
                false
            );
        }
    }

    function settleInternal(PoolInfo storage _pool, uint256 _seriesId)
        internal
        returns (uint32 minTickId, uint32 maxTickId)
    {
        LockedOptionStatePerTick[] memory lockedOptionStates = _pool.locked[_seriesId];

        minTickId = MAX_TICK;
        maxTickId = MIN_TICK;

        for (uint256 i = 0; i < lockedOptionStates.length; i++) {
            uint32 tickId = lockedOptionStates[i].tickId;
            Tick storage tick = _pool.ticks[tickId];

            if (minTickId > tickId) {
                minTickId = tickId;
            }

            if (maxTickId < tickId) {
                maxTickId = tickId;
            }

            if (lockedOptionStates[i].isLong) {
                settleLongPositions(_pool, tick, lockedOptionStates[i], _seriesId);
            }
        }

        delete _pool.locked[_seriesId];
    }

    function settleExpiryInternal(
        PoolInfo storage _pool,
        uint256 _expiryId,
        uint32 _tickId,
        Tick storage _tick
    ) internal returns (uint128) {
        Profit memory profit = _pool.profits[_expiryId][_tickId];

        uint128 cumulativeFee = profit.cumulativeFee;
        // calculate protocol fee
        uint128 protocolFee = cumulativeFee / _pool.configs[PROTOCOL_FEE_RATIO];

        _tick.balance += (cumulativeFee - protocolFee);
        _tick.unrealizedPnL -= profit.unrealizedPnL;

        return protocolFee;
    }

    function settleLongPositions(
        PoolInfo storage _pool,
        Tick storage _tick,
        LockedOptionStatePerTick memory state,
        uint256 _seriesId
    ) internal {
        (, uint128 longSize) = _pool.optionVault.getPositionSize(state.tickId, _seriesId);

        uint128 payout = _pool.optionVault.claim(_seriesId, longSize);

        _tick.balance += payout;
    }

    /**
     * @notice withdraw unrequired collaterals
     */
    function rebalanceCollateral(
        PoolInfo storage _pool,
        uint32 _tickId,
        uint256 _expiryId
    ) external {
        Tick storage tick = _pool.ticks[_tickId];

        uint128 unrequiredCollateralAmount = calculateUnrequiredCollateral(_pool, _tickId, _expiryId);

        if (unrequiredCollateralAmount == 0) {
            return;
        }

        tick.balance += unrequiredCollateralAmount;

        _pool.optionVault.withdraw(_tickId, _expiryId, unrequiredCollateralAmount);
    }

    //////////////////////
    // Getter Functions //
    //////////////////////

    function getMintAmount(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _amount
    ) external view returns (uint128) {
        uint128 share;
        uint128 range = _tickEnd - _tickStart;
        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick memory tick = _pool.ticks[i];
            if (tick.lastSupply > 0) {
                share += (1e6 * tick.lastBalance) / tick.lastSupply;
            } else {
                share += 1e6;
            }
        }
        return PredyMath.mulDiv(_amount * 1e6, range, share, false);
    }

    function getWithdrawableAmount(
        PoolInfo storage _pool,
        uint32 _tickStart,
        uint32 _tickEnd,
        uint128 _burn
    ) external view returns (uint128 withdrawableAmount) {
        require(_burn % (_tickEnd - _tickStart) == 0, "PoolLib: burn is not multiples of range length");
        uint128 burnPerRange = _burn / (_tickEnd - _tickStart);

        for (uint32 i = _tickStart; i < _tickEnd; i++) {
            Tick memory tick = _pool.ticks[i];
            withdrawableAmount += calSwapAmountForLPToken(tick, burnPerRange, false);
        }
    }

    /**
     * @notice get tick cumulative of seconds per liquidity
     * @param _ticks ticks
     * @param _tickLower lower tick to get seconds per liquidity
     * @param _tickUpper upper tick to get seconds per liquidity
     * @return secondsPerLiquidity scaled by 1e8
     */
    function getSecondsPerLiquidity(
        mapping(uint32 => Tick) storage _ticks,
        uint32 _tickLower,
        uint32 _tickUpper
    ) external view returns (uint128) {
        uint128 secondsPerLiquidity;
        for (uint32 i = _tickLower; i < _tickUpper; i++) {
            Tick memory tick = _ticks[i];
            secondsPerLiquidity += tick.secondsPerLiquidity;
        }
        return secondsPerLiquidity * (_tickUpper - _tickLower);
    }

    ///////////////////////
    // Private Functions //
    ///////////////////////

    /**
     * @notice remove balance from a tick
     */
    function removeBalancePerTick(
        Tick storage _tick,
        uint128 _burn,
        uint128 _reservedAmount
    ) internal returns (uint128) {
        // decrease balance
        uint128 substraction = calSwapAmountForLPToken(_tick, _burn, false);

        require(_tick.balance >= _reservedAmount + substraction, "AMMLib: no enough balance to withdraw");
        _tick.balance -= substraction;

        // decrease supply
        require(_tick.supply >= _burn, "AMMLib: burn is too big");
        _tick.supply -= _burn;

        return substraction;
    }

    function updateSecondsPerLiquidity(PoolInfo storage _pool, uint32 _currentTickId) internal {
        Tick storage state = _pool.ticks[_currentTickId];
        state.updateSecondsPerLiquidity(1e8 * getElapsedTime(_pool.lastBoughtTimestamp));
    }

    /**
     * @notice update seconds per liquidity
     * secondsPerLiquidity is scaled by 1e8
     * @param _tick a tick
     * @param _seconds seconds scaled by 1e8
     */
    function updateSecondsPerLiquidity(Tick storage _tick, uint128 _seconds) internal {
        if (_tick.supply > 0) {
            _tick.secondsPerLiquidity += (1e8 * _seconds) / (_tick.supply);
        }
    }

    /**
     * @notice internal function of calculating premium
     */
    function _calculatePremium(
        PoolInfo storage _pool,
        TradeState memory _step,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _spot,
        bool _isSelling
    ) internal view returns (uint128 totalPremium) {
        while (_step.remain != 0) {
            uint128 premium;
            uint128 fee;
            bool isContinue;

            if (_isSelling) {
                (premium, isContinue) = calculateTradeStateToSell(_pool, _spot, _series, _step);
            } else {
                (premium, fee, isContinue) = calculateTradeStateToBuy(_pool, _spot, _series, _step);
            }

            if (isContinue) {
                continue;
            }

            totalPremium += premium + fee;

            _step.currentTick = _step.nextTick;
        }
        return totalPremium;
    }

    /**
     * @notice process buying options
     * calculate premium and trade state and update ticks
     */
    function _buy(
        PoolInfo storage _pool,
        TradeState memory _step,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _spot
    ) internal returns (uint128 totalPremium, TradeState memory) {
        updateSecondsPerLiquidity(_pool, _step.currentTick);

        while (_step.remain != 0) {
            _step.isTickLong = getIsTickLongFlag(_pool, _series.seriesId, _step.currentTick, false);

            (uint128 premium, uint128 fee, bool isContinue) = calculateTradeStateToBuy(_pool, _spot, _series, _step);
            if (isContinue) {
                continue;
            }
            totalPremium += premium + fee;

            if (_step.isTickLong) {
                // remove long
                removeLongPosition(_pool, _step, _series.expiryId, _series.seriesId, premium, fee);
            } else {
                // add short
                addShortPosition(_pool, _step, _series, premium, fee);
            }

            _step.currentTick = _step.nextTick;
        }
        return (totalPremium, _step);
    }

    /**
     * @notice process buying options for a tick
     * calculate available size of a tick, next IV and premium
     */
    function calculateTradeStateToBuy(
        PoolInfo storage _pool,
        uint128 _spotPrice,
        IOptionVault.OptionSeriesView memory _series,
        TradeState memory _step
    )
        internal
        view
        returns (
            uint128 premium,
            uint128 fee,
            bool
        )
    {
        require(_step.currentTick < MAX_TICK, "AMMLib: tick is too large");

        Tick memory state = _pool.ticks[_step.currentTick];
        uint128 upper = tick2pos(_step.currentTick + 1);

        if (
            state.balance <=
            getUnavailableCollateral(state) + _pool.lockedAmounts[_step.currentTick][_series.seriesId][0] ||
            upper <= _step.currentIV
        ) {
            _step.currentTick += 1;
            return (0, 0, true);
        }
        {
            uint128 lower = tick2pos(_step.currentTick);
            if (_step.currentIV < lower) {
                _step.currentIV = lower;
            }
        }
        uint128 x1;
        {
            // calculate available size
            uint128 available = (state.balance -
                getUnavailableCollateral(state) -
                _pool.lockedAmounts[_step.currentTick][_series.seriesId][0]) / 2;
            uint128 ivMove;
            (available, ivMove) = calAvailableSizeForBuying(
                _pool,
                available,
                _series,
                _step.currentTick,
                upper - _step.currentIV
            );
            if (available >= _step.remain) {
                _step.stepAmount = _step.remain;
                _step.remain = 0;
                _step.nextTick = _step.currentTick;
            } else {
                _step.stepAmount = available;
                // _step.stepAmount must be less than _step.remain
                _step.remain -= _step.stepAmount;
                _step.nextTick = _step.currentTick + 1;
            }

            x1 = _step.currentIV + PredyMath.mulDiv(ivMove, _step.stepAmount, 1e12, true);
        }

        premium = calculatePrice(_pool, _spotPrice, _series, _step.currentIV, x1);
        fee = calculateSpread(_pool, _step.stepAmount, _spotPrice, premium);
        premium = (premium * _step.stepAmount) / 1e10;

        _step.currentIV = x1;
        _step.pricePerSize = (1e12 * (premium + fee)) / _step.stepAmount;

        premium = checkPricePerSize(_pool, _step, _series.seriesId, false, premium + fee) - fee;

        return (premium, fee, false);
    }

    /**
     * @notice sell options
     * trader can only sell to close if the same option series is shorted.
     * @param _pool option id
     * @param _step step information
     * @param _series option
     * @param _spot spot price
     * @return totalPremium total premium
     */
    function _sell(
        PoolInfo storage _pool,
        TradeState memory _step,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _spot
    ) internal returns (uint128 totalPremium, TradeState memory) {
        updateSecondsPerLiquidity(_pool, _step.currentTick);

        while (_step.remain != 0) {
            (uint128 premium, bool isContinue) = calculateTradeStateToSell(_pool, _spot, _series, _step);
            if (isContinue) {
                continue;
            }
            totalPremium += premium;

            if (_step.isTickLong) {
                // add long
                addLongPosition(_pool, _step, _series.expiryId, _series.seriesId, premium);
            } else {
                // remove short
                removeShortPosition(_pool, _step, _series, premium);
            }

            _step.currentTick = _step.nextTick;
        }

        require(_step.remain == 0, "AMMLib: no enough available balance");

        return (totalPremium, _step);
    }

    function calculateTradeStateToSell(
        PoolInfo storage _pool,
        uint128 _spotPrice,
        IOptionVault.OptionSeriesView memory _series,
        TradeState memory _step
    ) internal view returns (uint128 totalPremium, bool) {
        _step.isTickLong = getIsTickLongFlag(_pool, _series.seriesId, _step.currentTick, true);

        Tick memory state = _pool.ticks[_step.currentTick];
        uint128 lower = tick2pos(_step.currentTick);

        if (
            state.balance <=
            getUnavailableCollateral(state) + _pool.lockedAmounts[_step.currentTick][_series.seriesId][1] ||
            _step.currentIV <= lower
        ) {
            require(_step.currentTick > MIN_TICK, "AMMLib: tick is too small");
            _step.currentTick -= 1;
            return (0, true);
        }
        {
            uint128 upper = tick2pos(_step.currentTick + 1);
            if (_step.currentIV > upper) {
                _step.currentIV = upper;
            }
        }

        // calculate start IV and end IV
        uint128 x1 = _step.currentIV;
        {
            (uint128 available, uint128 ivMove) = calAvailableSizeForSelling(
                _pool,
                (state.balance -
                    getUnavailableCollateral(state) -
                    _pool.lockedAmounts[_step.currentTick][_series.seriesId][1]) / 2,
                _spotPrice,
                _series,
                lower,
                _step.currentIV,
                _step.currentTick
            );

            // available must be greater than 0
            if (available >= _step.remain) {
                _step.stepAmount = _step.remain;
                _step.remain = 0;
                _step.nextTick = _step.currentTick;
            } else {
                _step.stepAmount = available;
                // _step.stepAmount must be less than _step.remain
                _step.remain -= _step.stepAmount;
                require(_step.currentTick > MIN_TICK, "AMMLib: tick is too small");
                _step.nextTick = _step.currentTick - 1;
            }

            _step.currentIV -= PredyMath.mulDiv(_step.stepAmount, ivMove, 1e12, true);
        }
        {
            uint128 premium = calculatePrice(_pool, _spotPrice, _series, _step.currentIV, x1);
            premium = premium2c(premium, _step.stepAmount);

            totalPremium += premium;
        }

        _step.pricePerSize = (1e12 * totalPremium) / _step.stepAmount;

        totalPremium = checkPricePerSize(_pool, _step, _series.seriesId, true, totalPremium);

        return (totalPremium, false);
    }

    /**
     * @notice check lastPricePerSize to avoid sandwich attack.
     * compare lastPricePerSize and current pricePerSize, and re-calculate premium if needed.
     */
    function checkPricePerSize(
        PoolInfo storage _pool,
        TradeState memory _step,
        uint256 _seriesId,
        bool _isSelling,
        uint128 _price
    ) internal view returns (uint128 _priceAfter) {
        _priceAfter = _price;

        (LockedOptionStatePerTick memory locked, ) = getLockedOptionStatePerTick(_pool, _seriesId, _step.currentTick);

        if (locked.lastPricePerSize == 0) {
            return _price;
        }

        // premium never re-calculated after SAFETY_PERIOD
        if (locked.tradeTime + SAFETY_PERIOD <= block.timestamp) {
            return _price;
        }

        if (_isSelling) {
            // sell premium must be less than last buy's
            if (_step.pricePerSize > locked.lastPricePerSize) {
                _priceAfter = (locked.lastPricePerSize * _step.stepAmount) / 1e12;
                _step.pricePerSize = locked.lastPricePerSize;
            }
        } else {
            // buy premium must be greater than last sell's
            if (_step.pricePerSize < locked.lastPricePerSize) {
                _priceAfter = (locked.lastPricePerSize * _step.stepAmount) / 1e12;
                _step.pricePerSize = locked.lastPricePerSize;
            }
        }
    }

    /**
     * @notice add long position to pool
     */
    function addLongPosition(
        PoolInfo storage _pool,
        TradeState memory _step,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _premium
    ) internal {
        uint32 tickId = _step.currentTick;

        Tick storage tick = _pool.ticks[tickId];

        // deposit USDC to the vault for delta hedge
        uint128 lockAmount = _pool.optionVault.calRequiredMarginForASeries(
            _seriesId,
            -_step.stepAmount.toInt128(),
            IOptionVault.MarginLevel.Safe
        );

        _pool.optionVault.deposit(tickId, _expiryId, lockAmount);

        // decrease premium to pay
        require(tick.balance >= (_premium + lockAmount), "AMMLib: no enough balance");
        tick.balance -= (_premium + lockAmount);

        // add unrealized loss
        int128 premium = _premium.toInt128();

        tick.unrealizedPnL -= premium;
        _pool.profits[_expiryId][tickId].unrealizedPnL -= premium;

        // additional locked amount
        _pool.lockedAmounts[tickId][_seriesId][1] += _premium;

        // add options to the account
        _pool.optionVault.addLong(tickId, _expiryId, _seriesId, _step.stepAmount);

        // add series specific state
        for (uint256 i = 0; i < _pool.locked[_seriesId].length; i++) {
            LockedOptionStatePerTick storage locked = _pool.locked[_seriesId][i];
            if (locked.tickId == tickId) {
                require(locked.isLong);

                // update the last price per size
                locked.tradeTime = uint128(block.timestamp);
                locked.lastPricePerSize = _step.pricePerSize;
                return;
            }
        }

        // if there is no state, add new
        _pool.locked[_seriesId].push(
            LockedOptionStatePerTick(tickId, true, _step.pricePerSize, uint128(block.timestamp))
        );
    }

    /**
     * @notice remove long position from pool
     */
    function removeLongPosition(
        PoolInfo storage _pool,
        TradeState memory _step,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _premium,
        uint128 _fee
    ) internal {
        uint32 tickId = _step.currentTick;

        Tick storage tick = _pool.ticks[tickId];

        // add received premium to balance
        tick.balance += _premium;

        // add unrealized profit from premium
        int128 premium = _premium.toInt128();

        tick.unrealizedPnL += premium;

        _pool.profits[_expiryId][tickId].unrealizedPnL += premium;
        _pool.profits[_expiryId][tickId].cumulativeFee += _fee;

        // remove options from the account
        _pool.optionVault.removeLong(tickId, _expiryId, _seriesId, _step.stepAmount);

        // calculate unrequired collateral and withdraw the collateral
        uint128 unrequiredCollateral = _pool.optionVault.closeShortPosition(tickId, _seriesId, 0, 1e6);

        tick.balance += unrequiredCollateral;

        // decrease additional locked premium
        if (_pool.lockedAmounts[tickId][_seriesId][1] > _premium) {
            _pool.lockedAmounts[tickId][_seriesId][1] -= _premium;
        } else {
            _pool.lockedAmounts[tickId][_seriesId][1] = 0;
        }

        // transfer options to the trader
        ERC1155(address(_pool.optionVault)).safeTransferFrom(
            address(this),
            _step.trader,
            _seriesId,
            _step.stepAmount,
            ""
        );

        // update series specific state
        for (uint256 i = 0; i < _pool.locked[_seriesId].length; i++) {
            LockedOptionStatePerTick storage locked = _pool.locked[_seriesId][i];
            if (locked.tickId == tickId) {
                require(locked.isLong);

                // update the last price per size
                locked.tradeTime = uint128(block.timestamp);
                locked.lastPricePerSize = _step.pricePerSize;
                return;
            }
        }

        // if there is no series state, revert
        revert("AMMLib: series state not found");
    }

    /**
     * @notice add short position to pool
     */
    function addShortPosition(
        PoolInfo storage _pool,
        TradeState memory _step,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _premium,
        uint128 _fee
    ) internal {
        uint32 tickId = _step.currentTick;

        // deposit 100% of IM collateral and premium, and write options(transfer options to the trader)
        uint128 lockAmount = _pool.optionVault.depositAndWrite(
            tickId,
            _series.seriesId,
            1e6,
            _step.stepAmount,
            _step.trader
        );

        require(lockAmount >= _premium, "AMMLib: lockAmount must be greater than premium");

        Tick storage tick = _pool.ticks[tickId];

        // decrease lockAmount from pool balance
        require(tick.balance >= lockAmount + getUnavailableCollateral(tick), "AMMLib: enough balance to lock");
        tick.balance -= lockAmount - _premium;

        // add unrealized profit from premium
        int128 premium = _premium.toInt128();

        tick.unrealizedPnL += premium;

        _pool.profits[_series.expiryId][tickId].unrealizedPnL += premium;
        _pool.profits[_series.expiryId][tickId].cumulativeFee += _fee;

        // calculate additional lock amount
        _pool.lockedAmounts[tickId][_series.seriesId][0] += lockAmount;

        // update series specific state(premium and fee)
        for (uint256 i = 0; i < _pool.locked[_series.seriesId].length; i++) {
            LockedOptionStatePerTick storage locked = _pool.locked[_series.seriesId][i];
            if (locked.tickId == tickId) {
                require(!locked.isLong);

                // update the last price per size
                locked.tradeTime = uint128(block.timestamp);
                locked.lastPricePerSize = _step.pricePerSize;
                return;
            }
        }

        // if there is no series state, create new
        _pool.locked[_series.seriesId].push(
            LockedOptionStatePerTick(tickId, false, _step.pricePerSize, uint128(block.timestamp))
        );
    }

    /**
     * @notice remove short position from pool
     */
    function removeShortPosition(
        PoolInfo storage _pool,
        TradeState memory _step,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _premium
    ) internal {
        uint32 tickId = _step.currentTick;

        // burn options from the account
        // calculate unrequired collateral and withdraw the collateral
        uint128 unrequiredCollateral = _pool.optionVault.closeShortPosition(
            tickId,
            _series.seriesId,
            _step.stepAmount,
            1e6
        );

        Tick storage tick = _pool.ticks[tickId];

        uint128 balance = tick.balance;

        // increase withdrawn collateral to balance
        balance += unrequiredCollateral;

        // decrease premium to pay
        balance -= _premium;

        tick.balance = balance;

        // add unrealized loss
        int128 premium = _premium.toInt128();

        tick.unrealizedPnL -= premium;

        _pool.profits[_series.expiryId][tickId].unrealizedPnL -= premium;

        // additional locked amount
        if (_pool.lockedAmounts[tickId][_series.seriesId][0] > unrequiredCollateral) {
            _pool.lockedAmounts[tickId][_series.seriesId][0] -= unrequiredCollateral;
        } else {
            _pool.lockedAmounts[tickId][_series.seriesId][0] = 0;
        }

        // update series specific state
        for (uint256 i = 0; i < _pool.locked[_series.seriesId].length; i++) {
            LockedOptionStatePerTick storage locked = _pool.locked[_series.seriesId][i];
            if (locked.tickId == tickId) {
                require(!locked.isLong);

                // update the last price per size
                locked.tradeTime = uint128(block.timestamp);
                locked.lastPricePerSize = _step.pricePerSize;
                return;
            }
        }

        // if there is no series state, revert
        revert("AMMLib: series state not found");
    }

    function calculateUnrequiredCollateral(
        PoolInfo storage _pool,
        uint32 _tickId,
        uint256 _expiryId
    ) internal view returns (uint128 amount) {
        uint128 accountCollateral = _pool.optionVault.getVault(_tickId, _expiryId).collateral;

        uint128 requiredCollateral = getSafeMargin(_pool, _tickId, _expiryId);

        if (requiredCollateral < accountCollateral) {
            return accountCollateral - requiredCollateral;
        }

        return 0;
    }

    /**
     * @notice get pool value quote
     * returns the sum of pool value and hedged value
     */
    function getPoolValueQuote(
        PoolInfo storage _pool,
        Tick memory _tick,
        uint32 _tickId
    ) internal view returns (uint128 total) {
        uint128 b = _tick.balance + _pool.optionVault.getCollateralValueQuote(_tickId);

        if (_tick.unrealizedPnL > 0) {
            return b - uint128(_tick.unrealizedPnL);
        } else {
            return b + uint128(-_tick.unrealizedPnL);
        }
    }

    function calSwapAmountForLPToken(
        Tick memory _tick,
        uint128 _amount,
        bool _isDeposit
    ) internal pure returns (uint128) {
        if (_tick.lastBalance == 0 || _tick.lastSupply == 0) {
            return _amount;
        } else {
            return PredyMath.mulDiv(_amount, _tick.lastBalance, _tick.lastSupply, _isDeposit);
        }
    }

    function calculatePrice(
        PoolInfo storage _pool,
        uint128 _spot,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _x0,
        uint128 _x1
    ) internal view returns (uint128) {
        uint256 p = PriceCalculator.calculatePrice2(
            _spot,
            _series.strike,
            _series.maturity,
            _x0,
            _x1,
            _series.isPut,
            _pool.configs[MIN_DELTA]
        );
        return uint128(p);
    }

    /**
     * @notice calculating spread of premium.
     * spread is 0.4% of size and 1% of premium
     * @param _amount option size scaled by 1e8
     * @param _spot oracle price scaled by 1e8
     * @param _premium premium scaled by 1e8
     * @return fee scaled by 1e6
     */
    function calculateSpread(
        PoolInfo storage _pool,
        uint128 _amount,
        uint128 _spot,
        uint128 _premium
    ) internal view returns (uint128) {
        return (_amount * (_spot / _pool.configs[BASE_SPREAD] + _premium / 100)) / (1e10);
    }

    /**
     * @notice initialize trade state
     */
    function initializeTradeState(
        uint128 _amount,
        address _trader,
        uint64 _iv
    ) internal pure returns (TradeState memory) {
        uint128 currentIV = _iv;
        uint32 tick = uint32(currentIV / 1e7);

        return TradeState(tick, tick, 0, _amount, currentIV, _trader, false, 0);
    }

    function getIsTickLongFlag(
        PoolInfo storage _pool,
        uint256 _seriesId,
        uint32 _tickId,
        bool _isSelling
    ) internal view returns (bool) {
        (LockedOptionStatePerTick memory locked, bool exists) = getLockedOptionStatePerTick(_pool, _seriesId, _tickId);
        return exists ? locked.isLong : _isSelling;
    }

    /**
     * @notice calculate available size for buying
     * if tick is long, available size is same as the size of tick's long position.
     * if tick is short, available size is (balance / collateral to lock).
     * @return (available size, ivMove)
     *  available size is the size that trader can buy
     *  ivMove is iv changes per size in this trade
     */
    function calAvailableSizeForBuying(
        PoolInfo storage _pool,
        uint128 _c,
        IOptionVault.OptionSeriesView memory _series,
        uint32 _tickId,
        uint128 _ivRange
    ) internal view returns (uint128, uint128) {
        (LockedOptionStatePerTick memory locked, bool exists) = getLockedOptionStatePerTick(
            _pool,
            _series.seriesId,
            _tickId
        );

        if (exists && locked.isLong) {
            // get pool's long position size
            (, uint128 longSize) = _pool.optionVault.getPositionSize(_tickId, _series.seriesId);

            return (longSize, (1e12 * _ivRange) / longSize);
        } else {
            uint128 safeMargin = _pool.optionVault.calRequiredMarginForASeries(
                _series.seriesId,
                1e8,
                IOptionVault.MarginLevel.Safe
            );
            uint128 availableSize = (1e8 * _c) / safeMargin;

            // ivMove is scaled by 1e12
            uint128 ivMove = (1e12 * _ivRange) / availableSize;

            return (availableSize, ivMove);
        }
    }

    /**
     * @notice calculate available size for selling
     * if tick is long, available size is (balance / (premium to pay + collateral to lock)).
     * if tick is short, available size is same as the size of tick's short position.
     * @return (available size, ivMove)
     *  available size is the size that trader can sell
     *  ivMove is iv changes per size in this trade
     */
    function calAvailableSizeForSelling(
        PoolInfo storage _pool,
        uint128 _c,
        uint128 _spot,
        IOptionVault.OptionSeriesView memory _series,
        uint128 _iv0,
        uint128 _iv1,
        uint32 _tickId
    ) internal view returns (uint128, uint128) {
        (LockedOptionStatePerTick memory locked, bool exists) = getLockedOptionStatePerTick(
            _pool,
            _series.seriesId,
            _tickId
        );

        uint128 ivRange = 1e12 * (_iv1 - _iv0);

        if (!exists || locked.isLong) {
            // pricePerAmount is estimation price when IV moves from lower to _step.position (price/amount)
            uint128 availableSize;
            {
                uint128 pricePerAmount = calculatePrice(_pool, _spot, _series, _iv0, _iv1);
                uint128 safeMargin = _pool.optionVault.calRequiredMarginForASeries(
                    _series.seriesId,
                    -1e8,
                    IOptionVault.MarginLevel.Safe
                );

                availableSize = (_c * 1e10) / (pricePerAmount + 1e2 * safeMargin);
            }

            uint128 ivMove = ivRange / availableSize;

            return (availableSize, ivMove);
        } else {
            // get pool's short position size
            uint128 availableSize = getVaultSize(_pool, _tickId, _series.seriesId);
            return (availableSize, ivRange / availableSize);
        }
    }

    function getVaultSize(
        PoolInfo storage _pool,
        uint32 _tickId,
        uint256 _seriesId
    ) internal view returns (uint128) {
        (uint128 shortSize, ) = _pool.optionVault.getPositionSize(_tickId, _seriesId);
        return shortSize;
    }

    function getUnavailableCollateral(Tick memory _tick) internal pure returns (uint128) {
        return _tick.reservationForWithdrawal;
    }

    /**
     * @notice convert premium to amount of collateral
     * @param _premium premium
     * @param _size size of option
     */
    function premium2c(uint128 _premium, uint128 _size) internal pure returns (uint128) {
        uint128 r = (_premium * _size) / 1e8;
        return r / 1e2;
    }

    function getLockedOptionStatePerTick(
        PoolInfo storage _pool,
        uint256 _seriesId,
        uint32 _tickId
    ) internal view returns (LockedOptionStatePerTick memory, bool exists) {
        for (uint256 i = 0; i < _pool.locked[_seriesId].length; i++) {
            if (_pool.locked[_seriesId][i].tickId == _tickId) {
                return (_pool.locked[_seriesId][i], true);
            }
        }

        return (LockedOptionStatePerTick(0, false, 0, 0), false);
    }

    /**
     * @notice set pool IV
     */
    function setIV(
        PoolInfo storage _poolInfo,
        uint256 _seriesId,
        uint128 _iv
    ) internal {
        _poolInfo.optionVault.setIV(_seriesId, _iv);
    }

    /**
     * @notice get elapsed time from latest bought
     */
    function getElapsedTime(uint128 _lastBoughtTimestamp) internal view returns (uint128) {
        return uint128(block.timestamp) - _lastBoughtTimestamp;
    }

    /**
     * @notice calculate safe collateral
     */
    function getSafeMargin(
        PoolInfo storage _pool,
        uint32 _tickId,
        uint256 _expiryId
    ) internal view returns (uint128) {
        return _pool.optionVault.getRequiredMargin(_tickId, _expiryId, IOptionVault.MarginLevel.Safe);
    }

    function validateTick(uint32 _tickId) public pure {
        require(MIN_TICK <= _tickId, "AMM: start must be greater than MIN");
        require(_tickId < MAX_TICK, "AMM: tick must be less than MAX");
    }

    function validateRange(uint32 _s, uint32 _e) external pure {
        require(MIN_TICK <= _s, "AMM: start must be greater than MIN");
        require(_s < _e, "AMM: end must be greater than start");
        require(_s < MAX_TICK && _e <= MAX_TICK, "AMM: tick must be less than MAX");
        require(_s > 0 && _e > 1, "AMM: tick must be greater than 1");
    }

    function tick2pos(uint32 _tick) internal pure returns (uint128) {
        if (_tick >= MAX_TICK) {
            return 1e7 * 50;
        } else {
            return 1e7 * _tick;
        }
    }
}

// File: contracts/PriceOracle.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

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

/**
 * @notice chainlink price oracle.
 * This is the contract to fix the price at the nearest time after expiry.
 */
contract PriceOracle is Ownable {
    /// @dev expiry price with timestamp
    struct ExpiryPrice {
        uint256 price;
        uint256 timestamp;
    }

    /// @dev chainlink aggregator address of the underlying asset price
    mapping(address => AggregatorV3Interface) aggregators;

    /// @dev aggregator address => timestamp => expiry price
    mapping(address => mapping(uint256 => ExpiryPrice)) internal expiryPrices;

    event ExpiryPriceUpdated(address aggregator, uint256 expiry, uint256 price);

    uint256 public constant DISPUTE_PERIOD = 2 hours;

    /**
     * @notice set aggregator
     * only owner can set new aggregator
     */
    function setAggregator(address _aggregatorAddress) external onlyOwner {
        require(address(aggregators[_aggregatorAddress]) == address(0));
        aggregators[_aggregatorAddress] = AggregatorV3Interface(_aggregatorAddress);
    }

    /**
     * @notice set expiry price
     * anyone can set price if the price has not been setted.
     * Also timestamp must be later than expiration.
     */
    function setExpiryPrice(address _aggregator, uint256 _expiryTimestamp) external {
        (uint256 price, uint256 timestamp) = getPrice(_aggregator);

        require(_expiryTimestamp < timestamp, "PriceOracle: price timestamp must be later than expiry");

        ExpiryPrice storage expiryPrice = expiryPrices[_aggregator][_expiryTimestamp];

        require(expiryPrice.timestamp == 0, "PriceOracle: already setted");

        expiryPrice.price = price;
        expiryPrice.timestamp = timestamp;

        emit ExpiryPriceUpdated(_aggregator, _expiryTimestamp, price);
    }

    /**
     * @notice update expiry price
     * anyone can update price if the price has not been setted
     * or if new price's timestamp is earlier than previous one.
     * Also timestamp must be later than expiration.
     */
    function updateExpiryPrice(
        address _aggregator,
        uint256 _expiryTimestamp,
        uint80 _roundId
    ) external {
        (uint256 price, uint256 timestamp) = getHistoricalPrice(_aggregator, _roundId);

        require(_expiryTimestamp < timestamp, "PriceOracle: price timestamp must be later than expiry");

        ExpiryPrice storage expiryPrice = expiryPrices[_aggregator][_expiryTimestamp];

        require(
            expiryPrice.timestamp == 0 || expiryPrice.timestamp > timestamp,
            "PriceOracle: new price's timestamp must be close to expiry"
        );

        expiryPrice.price = price;
        expiryPrice.timestamp = timestamp;

        emit ExpiryPriceUpdated(_aggregator, _expiryTimestamp, price);
    }

    /**
     * @notice get price for an expiration
     * @return price price scaled by 1e8
     * @return _isFinalized returns true if price has been finalized, if not returns false
     */
    function getExpiryPrice(address _aggregator, uint256 _expiryTimestamp)
        external
        view
        returns (uint256 price, bool _isFinalized)
    {
        price = expiryPrices[_aggregator][_expiryTimestamp].price;
        _isFinalized = isFinalized(_aggregator, _expiryTimestamp);
    }

    /**
     * @notice return flag if price is finalized or not
     * true if dispute period has been passed and price has been setted more than once
     */
    function isFinalized(address _aggregator, uint256 _expiryTimestamp) internal view returns (bool) {
        return
            (_expiryTimestamp + DISPUTE_PERIOD < block.timestamp) &&
            expiryPrices[_aggregator][_expiryTimestamp].timestamp != 0;
    }

    /**
     * @notice get price scaled by 1e8
     */
    function getPrice(address _aggregatorAddress) public view returns (uint256, uint256) {
        (, int256 answer, , uint256 roundTimestamp, ) = aggregators[_aggregatorAddress].latestRoundData();

        require(answer > 0, "PriceOracle: price is lower than 0");

        return (uint256(answer), roundTimestamp);
    }

    function getHistoricalPrice(address _aggregatorAddress, uint80 _roundId) public view returns (uint256, uint256) {
        (, int256 answer, , uint256 roundTimestamp, ) = aggregators[_aggregatorAddress].getRoundData(_roundId);

        require(answer > 0, "PriceOracle: price is lower than 0");

        return (uint256(answer), roundTimestamp);
    }
}

// File: contracts/OptionVault.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "./lib/OptionLib.sol";
import "./PriceOracle.sol";
import "./interfaces/IOptionVault.sol";
import "./interfaces/IAMM.sol";

/**
 * @notice OptionVault contract is vaults for basic on-chain option contracts.
 * It manages option tokens.
 */
contract OptionVault is IOptionVault, ERC1155, IERC1155Receiver {
    using OptionLib for OptionLib.OptionInfo;

    /// @dev operator address
    address operator;

    /// @dev AMM address
    address ammAddress;

    /// @dev option info
    OptionLib.OptionInfo public optionInfo;

    /// @dev price oracle contract
    PriceOracle priceOracle;

    // events
    event ExpiryCreated(uint256 indexed expiryId, uint64 expiry);
    event SeriesCreated(uint256 indexed expiryId, uint256 seriesId, uint128 strike, bool isPut);

    event AccountCreated(uint256 accountId, address indexed account);
    event VaultDeposited(uint256 indexed accountId, uint256 expiryId, uint128 amount);
    event VaultWithdrawn(uint256 indexed accountId, uint256 expiryId, uint128 amount);
    event Written(uint256 indexed accountId, uint256 seriesId, uint128 amount, address recipient);
    event Unlocked(uint256 indexed accountId, uint256 seriesId, uint128 amount, address holder);
    event Claimed(uint256 indexed seriesId, uint128 amount, uint128 profit);
    event Settled(uint256 accountId, uint256 indexed seriesId, uint128 profit);
    event Hedged(uint32 tickId, int256 tickDelta, int256 hedgePosition);
    event Liquidated(uint256 accountId, uint256 seriesId);
    event ConfigUpdated(uint8 key, uint128 value);

    // modifiers
    modifier onlyVaultOwner(uint256 _accountId) {
        // if accountId is id of trader's vaults, check owner
        // if AMM vaults, check caller is AMM contract
        require(
            (msg.sender == optionInfo.getVaultOwner(_accountId) &&
                _accountId >= OptionLib.MIN_VAULT_ID &&
                _accountId < optionInfo.vaultCount) ||
                (msg.sender == ammAddress && _accountId < OptionLib.MIN_VAULT_ID),
            "V1"
        );
        _;
    }

    modifier onlyAMMVault(uint256 _accountId) {
        require(_accountId < OptionLib.MIN_VAULT_ID, "V2");
        _;
    }

    modifier notAMMVault(uint256 _accountId) {
        require(_accountId >= OptionLib.MIN_VAULT_ID, "V3");
        _;
    }

    modifier onlyOperator() {
        require(msg.sender == operator, "V4");
        _;
    }

    modifier onlyAMM() {
        require(msg.sender == ammAddress, "V5");
        _;
    }

    constructor(
        string memory _uri,
        address _aggregator,
        address _collateral,
        address _underlying,
        address _priceOracle,
        address _operator,
        address _lendingPool
    ) ERC1155(_uri) {
        optionInfo.aggregator = _aggregator;
        priceOracle = PriceOracle(_priceOracle);

        operator = _operator;

        optionInfo.init(_collateral, _underlying, _lendingPool);
    }

    function onERC1155Received(
        address _operator,
        address from,
        uint256 id,
        uint256 value,
        bytes memory data
    ) external override(IERC1155Receiver) returns (bytes4) {
        return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
    }

    function onERC1155BatchReceived(
        address _operator,
        address from,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) external override(IERC1155Receiver) returns (bytes4) {
        return bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"));
    }

    function isApprovedForAll(address account, address _operator) public view virtual override returns (bool) {
        return super.isApprovedForAll(account, _operator) || _operator == address(this) || _operator == ammAddress;
    }

    /**
     * @notice set AMM address
     * @param _ammAddress amm address
     */
    function setAMMAddress(address _ammAddress) external {
        require(ammAddress == address(0));
        ammAddress = _ammAddress;
        setApprovalForAll(ammAddress, true);
    }

    /**
     * @notice set IV by AMM contract
     * @param _seriesId series id
     * @param _iv new implied volatility
     */
    function setIV(uint256 _seriesId, uint128 _iv) external override(IOptionVault) onlyAMM {
        optionInfo.setIV(_seriesId, _iv);
    }

    /**
     * @notice create new vault
     */
    function createAccount() public override(IOptionVault) returns (uint256) {
        uint256 accountId = optionInfo.createAccount(msg.sender);

        emit AccountCreated(accountId, msg.sender);

        return accountId;
    }

    /**
     * @notice deposit collateral to the vault
     * @param _accountId vault id
     * @param _collateral amount to deposit scaled by 1e6
     */
    function deposit(
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _collateral
    ) external override(IOptionVault) onlyVaultOwner(_accountId) {
        require(_collateral > 0, "V9");

        optionInfo.deposit(_accountId, _expiryId, _collateral);

        IERC20(optionInfo.tokens.collateral).transferFrom(msg.sender, address(this), _collateral);

        emit VaultDeposited(_accountId, _expiryId, _collateral);
    }

    /**
     * @notice withdraw collateral from the vault
     * @param _accountId vault id
     * @param _expiryId expiry id
     * @param _collateral amount to withdraw scaled by 1e6
     */
    function withdraw(
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _collateral
    ) external override(IOptionVault) onlyVaultOwner(_accountId) {
        require(_collateral > 0, "V9");

        uint128 spot = getPrice();

        optionInfo.withdraw(_accountId, _expiryId, _collateral, spot);

        IERC20(optionInfo.tokens.collateral).transfer(msg.sender, _collateral);

        emit VaultWithdrawn(_accountId, _expiryId, _collateral);
    }

    /**
     * @notice close short position and withdraw unrequired collateral from the vault
     * @param _accountId account id
     * @param _seriesId series id to close
     * @param _amount the amount to close
     * @param _cRatio the final IM ratio(IM / collateral)
     */
    function closeShortPosition(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _cRatio
    ) external override(IOptionVault) onlyVaultOwner(_accountId) returns (uint128) {
        require(0 < _cRatio && _cRatio <= 1e6, "V6");

        if (_amount > 0) {
            require(balanceOf(msg.sender, _seriesId) >= _amount, "V7");

            // unlock options
            optionInfo.unlock(_accountId, _seriesId, _amount);

            _burn(msg.sender, _seriesId, _amount);

            emit Unlocked(_accountId, _seriesId, _amount, msg.sender);
        }

        bool isPool = _accountId < OptionLib.MIN_VAULT_ID;

        uint256 expiryId = optionInfo.serieses[_seriesId].expiryId;
        uint128 unrequiredCollateral = optionInfo.withdrawUnrequiredCollateral(
            _accountId,
            expiryId,
            getPrice(),
            _cRatio,
            isPool
        );

        if (unrequiredCollateral > 0) {
            IERC20(optionInfo.tokens.collateral).transfer(msg.sender, unrequiredCollateral);

            emit VaultWithdrawn(_accountId, expiryId, unrequiredCollateral);
        }

        return unrequiredCollateral;
    }

    /**
     * @notice lock collateral and mint option tokens
     * @param _accountId vault id
     * @param _seriesId option series id
     * @param _amount amount to write scaled by 1e8
     * @param _recipient recipient of option tokens
     */
    function write(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        address _recipient
    ) external override(IOptionVault) onlyVaultOwner(_accountId) {
        require(_amount > 0, "V9");

        uint128 spot = getPrice();

        // write options
        optionInfo.write(_accountId, _seriesId, _amount, spot);

        _mint(_recipient, _seriesId, _amount, "");

        emit Written(_accountId, _seriesId, _amount, _recipient);
    }

    /**
     * @notice deposit collateral and write options in one transaction
     * @param _accountId vault id
     * @param _seriesId option series id
     * @param _cRatio _collateral ratio of Initial Margin
     * @param _amount amount to write scaled by 1e8
     * @param _recipient recipient of option tokens
     */
    function depositAndWrite(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _cRatio,
        uint128 _amount,
        address _recipient
    ) public override(IOptionVault) onlyVaultOwner(_accountId) returns (uint128) {
        require(_amount > 0, "V9");
        require(0 < _cRatio && _cRatio <= 1e6, "V6");

        bool isPool = _accountId < OptionLib.MIN_VAULT_ID;

        // deposit collateral and write options
        (uint256 expiryId, uint128 collateral) = optionInfo.depositAndWrite(
            _accountId,
            _seriesId,
            _amount,
            _cRatio,
            getPrice(),
            isPool
        );

        IERC20(optionInfo.tokens.collateral).transferFrom(msg.sender, address(this), collateral);

        _mint(_recipient, _seriesId, _amount, "");

        emit VaultDeposited(_accountId, expiryId, collateral);
        emit Written(_accountId, _seriesId, _amount, _recipient);

        return collateral;
    }

    /**
     * @notice add long position to calculate net delta
     */
    function addLong(
        uint256 _accountId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external override(IOptionVault) onlyVaultOwner(_accountId) {
        require(_amount > 0, "V9");

        uint128 longSize = optionInfo.addLong(_accountId, _expiryId, _seriesId, _amount);

        require(balanceOf(msg.sender, _seriesId) >= longSize, "V10");
    }

    /**
     * @notice remove long position to calculate net delta
     */
    function removeLong(
        uint256 _accountId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external override(IOptionVault) onlyVaultOwner(_accountId) {
        require(_amount > 0, "V9");

        optionInfo.removeLong(_accountId, _expiryId, _seriesId, _amount);
    }

    /**
     * @notice swap collateral asset for underling asset to hedge vault's delta
     * @param _accountId the id of target vault
     * @param _underlyingAmount amount of underlying asset scaled by 1e8
     * @param _collateralAmount amount of collateral asset scaled by 1e6
     */
    function addUnderlyingLong(
        uint32 _accountId,
        uint256 _expiryId,
        uint256 _underlyingAmount,
        uint256 _collateralAmount
    ) external onlyAMMVault(_accountId) {
        uint128 spot = getPrice();

        // calculate vault's net delta
        int256 vaultDelta = optionInfo.calculateVaultDelta(_accountId, _expiryId, spot);

        // swap collateral asset for underling asset
        int256 hedgePosition = optionInfo.addUnderlyingLong(
            _accountId,
            _expiryId,
            spot,
            vaultDelta,
            _underlyingAmount,
            _collateralAmount
        );

        emit Hedged(_accountId, vaultDelta, hedgePosition);
    }

    /**
     * @notice swap underling asset for collateral asset to hedge vaults' delta
     * @param _accountId the id of target vault
     * @param _underlyingAmount amount of underlying asset scaled by 1e8
     * @param _collateralAmount amount of collateral asset scaled by 1e6
     */
    function addUnderlyingShort(
        uint32 _accountId,
        uint256 _expiryId,
        uint256 _underlyingAmount,
        uint256 _collateralAmount
    ) external onlyAMMVault(_accountId) {
        uint128 spot = getPrice();

        // calculate vault's net delta
        int256 vaultDelta = optionInfo.calculateVaultDelta(_accountId, _expiryId, spot);

        // swap underling asset for collateral asset
        int256 hedgePosition = optionInfo.addUnderlyingShort(
            _accountId,
            _expiryId,
            spot,
            vaultDelta,
            _underlyingAmount,
            _collateralAmount
        );

        emit Hedged(_accountId, vaultDelta, hedgePosition);
    }

    /**
     * @notice repay all underlying asset and withdraw collateral from AAVE
     * @param _repayAmount amount to repay
     */
    function redeemCollateralFromLendingPool(uint128 _repayAmount) external onlyOperator {
        optionInfo.redeemCollateralFromLendingPool(
            _repayAmount,
            getPrice(),
            msg.sender,
            address(IAMM(ammAddress).feePool())
        );
    }

    /**
     * @notice burn options and get collateral from a vault that requires liquidation
     * to save the profit of option holders.
     * @param _accountId vault id
     * @param _seriesId option series id
     * @param _amount amount to liquidate scaled by 1e8
     */
    function liquidate(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount
    ) external notAMMVault(_accountId) {
        require(_amount > 0, "V9");

        require(balanceOf(msg.sender, _seriesId) >= _amount, "V7");

        uint128 price = getPrice();

        uint128 reward = optionInfo.liquidate(_accountId, _seriesId, _amount, price);

        IERC20(optionInfo.tokens.collateral).transfer(msg.sender, reward);

        _burn(msg.sender, _seriesId, _amount);

        emit Liquidated(_accountId, _seriesId);
    }

    /**
     * @notice claim profit of some amount of expired option contracts.
     * @param _seriesId option series id
     * @param _size option size to claim profit scaled by 1e8
     */
    function claim(uint256 _seriesId, uint128 _size) external override(IOptionVault) returns (uint128) {
        IOptionVault.OptionSeries storage series = optionInfo.serieses[_seriesId];

        require(balanceOf(msg.sender, _seriesId) >= _size, "V7");

        uint256 price = getExpiryPrice(series.expiryId);

        uint128 payout = optionInfo.claimProfit(_seriesId, _size, uint128(price));

        // burn options
        _burn(msg.sender, _seriesId, _size);

        // send payout to option holder
        IERC20(optionInfo.tokens.collateral).transfer(msg.sender, payout);

        emit Claimed(_seriesId, _size, payout);

        return payout;
    }

    /**
     * @notice settle a vault
     * fix the payout of an expired vault and send collateral in the vault to vault's owner
     * @param _accountId vault id
     * @param _expiryId option series id
     */
    function settleVault(uint256 _accountId, uint256 _expiryId) external override(IOptionVault) returns (uint128) {
        // anyone can settle trader's vault, but only AMM contract can settle AMM's vault
        require(
            (_accountId >= OptionLib.MIN_VAULT_ID && _accountId < optionInfo.vaultCount) ||
                (msg.sender == ammAddress && _accountId < OptionLib.MIN_VAULT_ID),
            "V8"
        );

        uint256 price = getExpiryPrice(_expiryId);

        uint128 settledAmount = optionInfo.settle(_accountId, _expiryId, uint128(price));

        if (settledAmount > 0) {
            // send all vault's collateral to owner
            if (_accountId < OptionLib.MIN_VAULT_ID) {
                IERC20(optionInfo.tokens.collateral).transfer(ammAddress, settledAmount);
            } else {
                IERC20(optionInfo.tokens.collateral).transfer(optionInfo.getVaultOwner(_accountId), settledAmount);
            }
        }

        emit Settled(_accountId, _expiryId, settledAmount);

        return settledAmount;
    }

    ////////////////////////
    // Wrapper Functions //
    ////////////////////////

    function makeShortPosition(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _cRatio,
        uint128 _amount,
        uint128 _minFee
    ) external {
        uint256 accountId = _accountId;

        if (accountId == 0) {
            accountId = createAccount();
        }

        depositAndWrite(accountId, _seriesId, _cRatio, _amount, address(this));

        uint128 premium = IAMM(ammAddress).sell(_seriesId, _amount, _minFee);

        IERC20(optionInfo.tokens.collateral).transfer(msg.sender, premium);
    }

    ////////////////////////
    // Operator Functions //
    ////////////////////////

    /**
     * @notice create new expiration
     * @param _expiry expiration
     * @param _strikes strike prices
     * @param _callIVs initial call ivs
     * @param _putIVs initial put ivs
     */
    function createExpiry(
        uint64 _expiry,
        uint64[] memory _strikes,
        uint64[] memory _callIVs,
        uint64[] memory _putIVs
    ) external onlyOperator {
        uint128 expiryId = optionInfo.createExpiry(_expiry);

        for (uint256 i = 0; i < _strikes.length; i++) {
            createSeries(expiryId, _strikes[i], false, _callIVs[i]);
            createSeries(expiryId, _strikes[i], true, _putIVs[i]);
        }

        emit ExpiryCreated(expiryId, _expiry);
    }

    /**
     * @notice update a config value
     */
    function setConfig(uint8 _key, uint128 _value) external onlyOperator {
        require(_value > 0);
        optionInfo.setConfig(_key, _value);

        // emit event
        emit ConfigUpdated(_key, _value);
    }

    /**
     * @notice set new operator
     * @param _operator operator address
     */
    function setNewOperator(address _operator) external onlyOperator {
        operator = _operator;
    }

    //////////////////////
    // Getter Functions //
    //////////////////////

    /**
     * @notice get an expiration
     */
    function getExpiration(uint256 _expiryId)
        external
        view
        override(IOptionVault)
        returns (IOptionVault.Expiration memory)
    {
        return optionInfo.expiries[_expiryId];
    }

    /**
     * @notice get an option series
     */
    function getOptionSeries(uint256 _seriesId)
        external
        view
        override(IOptionVault)
        returns (IOptionVault.OptionSeriesView memory)
    {
        return optionInfo.getOptionSeriesView(_seriesId);
    }

    /**
     * @notice get list of live option serieses
     */
    function getLiveOptionSerieses() external view override(IOptionVault) returns (IOptionVault.Expiration[] memory) {
        return optionInfo.getLiveOptionSerieses();
    }

    /**
     * @notice get timestamp of the last expiry
     */
    function getLastExpiry() external view override(IOptionVault) returns (uint64) {
        return optionInfo.expiries[optionInfo.expiryCount - 1].expiry;
    }

    /**
     * @notice get total collateral value of a vault
     * @param _accountId vault id
     */
    function getCollateralValueQuote(uint256 _accountId) external view override(IOptionVault) returns (uint128) {
        return optionInfo.getCollateralValueQuote(_accountId, getPrice());
    }

    /**
     * @notice get required margin of a vault
     * there are 3 margin levels
     * 1. Maintenance Margin: vaults can be liquidated if the collateral is lower than MM
     * 2. Initial Margin: collateral must be greater than IM to write options
     * 3. Safe Margin: enough margin for delta hedging
     * @param _accountId vault id
     * @param _expiryId expiry id
     * @param _marginLevel margin level
     */
    function getRequiredMargin(
        uint256 _accountId,
        uint256 _expiryId,
        IOptionVault.MarginLevel _marginLevel
    ) external view override(IOptionVault) returns (uint128) {
        uint128 price = getPrice();

        return optionInfo.getRequiredMargin(_accountId, _expiryId, price, _marginLevel);
    }

    /**
     * @notice calculate required margin for a series
     * @param _seriesId series id
     * @param _amount amount of options. plus for short and minus for long
     * @param _marginLevel margin level
     */
    function calRequiredMarginForASeries(
        uint256 _seriesId,
        int128 _amount,
        IOptionVault.MarginLevel _marginLevel
    ) external view override(IOptionVault) returns (uint128) {
        IOptionVault.OptionSeries memory series = optionInfo.serieses[_seriesId];

        return optionInfo.getRequiredMarginForASeries(series.expiryId, _seriesId, getPrice(), _amount, _marginLevel);
    }

    /**
     * @notice get total payout of a vault
     * @param _accountId vault id
     * @param _expiryId expiry id
     */
    function getTotalPayout(uint256 _accountId, uint256 _expiryId)
        external
        view
        override(IOptionVault)
        returns (uint128)
    {
        return optionInfo.getTotalPayout(_accountId, _expiryId, getPrice(), false);
    }

    /**
     * @notice get liquidatable amount of a vault
     * @param _accountId vault id
     * @param _seriesId series id
     */
    function getLiquidatableAmount(uint256 _accountId, uint256 _seriesId) external view returns (uint128 limit) {
        return optionInfo.getLiquidatableAmount(_accountId, _seriesId, getPrice());
    }

    function getAccount(uint256 _accountId) external view override(IOptionVault) returns (AccountView memory) {
        Account storage account = optionInfo.accounts[_accountId];
        return AccountView(account.owner, account.settledCount);
    }

    function getVault(uint256 _accountId, uint256 _expiryId)
        external
        view
        override(IOptionVault)
        returns (VaultView memory)
    {
        Vault storage vault = optionInfo.accounts[_accountId].vaults[_expiryId];
        return
            VaultView(
                optionInfo.accounts[_accountId].owner,
                vault.isSettled,
                vault.collateral,
                vault.hedgePosition,
                vault.shortLiquidity
            );
    }

    /**
     * @notice get position size
     * @param _accountId vault id
     * @param _seriesId option series id
     * @return (short size, long size)
     */
    function getPositionSize(uint256 _accountId, uint256 _seriesId)
        external
        view
        override(IOptionVault)
        returns (uint128, uint128)
    {
        return optionInfo.getPositionSize(_accountId, _seriesId);
    }

    /**
     * @notice get vault's net delta
     * @param _accountId vault id
     */
    function calculateVaultDelta(uint256 _accountId, uint256 _expiryId)
        external
        view
        override(IOptionVault)
        returns (int256)
    {
        return optionInfo.calculateVaultDelta(_accountId, _expiryId, getPrice());
    }

    /**
     * @notice get a config value
     */
    function getConfig(uint8 _key) external view returns (uint128) {
        return optionInfo.configs[_key];
    }

    ///////////////////////
    // Private Functions //
    ///////////////////////

    function createSeries(
        uint128 _expiryId,
        uint64 _strike,
        bool _isPut,
        uint64 _iv
    ) internal {
        uint256 seriesId = optionInfo.createSeries(_expiryId, _strike, _isPut, _iv);
        emit SeriesCreated(_expiryId, seriesId, _strike, _isPut);
    }

    function getExpiryPrice(uint256 _expiryId) internal view returns (uint256) {
        IOptionVault.Expiration storage expiration = optionInfo.expiries[_expiryId];

        (uint256 price, bool isFinalized) = priceOracle.getExpiryPrice(optionInfo.aggregator, expiration.expiry);

        require(isFinalized, "V11");

        return price;
    }

    function getPrice() internal view returns (uint128) {
        (uint256 spot, ) = priceOracle.getPrice(optionInfo.aggregator);
        return uint128(spot);
    }
}

// File: contracts/interfaces/IOptionVault.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

interface IOptionVault {
    enum MarginLevel {
        Maintenance,
        Initial,
        Safe
    }

    struct Expiration {
        uint128 expiryId;
        uint64 expiry;
        uint256[] seriesIds;
    }

    struct OptionSeries {
        uint64 strike;
        bool isPut;
        uint64 iv;
        uint128 expiryId;
    }

    struct Account {
        address owner;
        uint256 settledCount;
        mapping(uint256 => Vault) vaults;
    }

    struct Vault {
        // is settled or not
        bool isSettled;
        // seriesId => short amount
        mapping(uint256 => uint128) shorts;
        // seriesId => long amount
        mapping(uint256 => uint128) longs;
        //
        uint128 collateral;
        //
        int256 hedgePosition;
        //
        uint128 shortLiquidity;
    }

    // view struct

    struct OptionSeriesParams {
        uint256 id;
        uint64 maturity;
        uint128 strike;
        bool isPut;
        uint128 iv;
    }

    struct OptionSeriesView {
        uint256 expiryId;
        uint256 seriesId;
        uint64 expiry;
        uint64 maturity;
        uint128 strike;
        bool isPut;
        uint64 iv;
    }

    struct AccountView {
        address owner;
        uint256 settledCount;
    }

    struct VaultView {
        address owner;
        bool isSettled;
        uint128 collateral;
        int256 hedgePosition;
        uint128 shortLiquidity;
    }

    function getExpiration(uint256 _expiryId) external view returns (IOptionVault.Expiration memory);

    function getOptionSeries(uint256 _seriesId) external view returns (IOptionVault.OptionSeriesView memory);

    function getAccount(uint256 _vaultId) external view returns (AccountView memory);

    function getVault(uint256 _vaultId, uint256 _expiryId) external view returns (VaultView memory);

    function getCollateralValueQuote(uint256 _vaultId) external view returns (uint128);

    function getRequiredMargin(
        uint256 _vaultId,
        uint256 _expiryId,
        IOptionVault.MarginLevel _marginLevel
    ) external view returns (uint128);

    function calRequiredMarginForASeries(
        uint256 _seriesId,
        int128 _amount,
        IOptionVault.MarginLevel _marginLevel
    ) external view returns (uint128);

    function getTotalPayout(uint256 _vaultId, uint256 _expiryId) external view returns (uint128);

    function getPositionSize(uint256 _vaultId, uint256 _seriesId) external view returns (uint128, uint128);

    function getLiveOptionSerieses() external view returns (IOptionVault.Expiration[] memory);

    function getLastExpiry() external view returns (uint64);

    function createAccount() external returns (uint256);

    function setIV(uint256 _seriesId, uint128 _iv) external;

    function deposit(
        uint256 _vaultId,
        uint256 _expiryId,
        uint128 _collateral
    ) external;

    function withdraw(
        uint256 _vaultId,
        uint256 _expiryId,
        uint128 _collateral
    ) external;

    function closeShortPosition(
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _cRatio
    ) external returns (uint128);

    function write(
        uint256 _vaultId,
        uint256 _seriesId,
        uint128 _amount,
        address _recepient
    ) external;

    function depositAndWrite(
        uint256 _vaultId,
        uint256 _seriesId,
        uint128 _collateral,
        uint128 _amount,
        address _recepient
    ) external returns (uint128);

    function settleVault(uint256 _vaultId, uint256 _expiryId) external returns (uint128);

    function claim(uint256 _seriesId, uint128 _size) external returns (uint128);

    function addLong(
        uint256 _vaultId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external;

    function removeLong(
        uint256 _vaultId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external;

    function calculateVaultDelta(uint256 _vaultId, uint256 _expiryId) external view returns (int256);
}

// File: contracts/interfaces/IFeePool.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

interface IFeePool {
    function sendProfitERC20(address _account, uint256 _amount) external;
}

// File: contracts/interfaces/IAMM.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "./IFeePool.sol";

interface IAMM {
    function sell(
        uint256 _seriesId,
        uint128 _amount,
        uint128 _minFee
    ) external returns (uint128);

    function feePool() external returns (IFeePool);
}

// File: @openzeppelin/contracts/token/ERC1155/IERC1155.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC1155 compliant contract, as defined in the
 * https://eips.ethereum.org/EIPS/eip-1155[EIP].
 *
 * _Available since v3.1._
 */
interface IERC1155 is IERC165 {
    /**
     * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`.
     */
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

    /**
     * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
     * transfers.
     */
    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);

    /**
     * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
     * `approved`.
     */
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    /**
     * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
     *
     * If an {URI} event was emitted for `id`, the standard
     * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
     * returned by {IERC1155MetadataURI-uri}.
     */
    event URI(string value, uint256 indexed id);

    /**
     * @dev Returns the amount of tokens of token type `id` owned by `account`.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function balanceOf(address account, uint256 id) external view returns (uint256);

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     */
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);

    /**
     * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
     *
     * Emits an {ApprovalForAll} event.
     *
     * Requirements:
     *
     * - `operator` cannot be the caller.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
     *
     * See {setApprovalForAll}.
     */
    function isApprovedForAll(address account, address operator) external view returns (bool);

    /**
     * @dev Transfers `amount` tokens of token type `id` from `from` to `to`.
     *
     * Emits a {TransferSingle} event.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}.
     * - `from` must have a balance of tokens of type `id` of at least `amount`.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
     * acceptance magic value.
     */
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
     *
     * Emits a {TransferBatch} event.
     *
     * Requirements:
     *
     * - `ids` and `amounts` must have the same length.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
     * acceptance magic value.
     */
    function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
}

// File: @openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../IERC1155.sol";

/**
 * @dev Interface of the optional ERC1155MetadataExtension interface, as defined
 * in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[EIP].
 *
 * _Available since v3.1._
 */
interface IERC1155MetadataURI is IERC1155 {
    /**
     * @dev Returns the URI for token type `id`.
     *
     * If the `\{id\}` substring is present in the URI, it must be replaced by
     * clients with the actual token type ID.
     */
    function uri(uint256 id) external view returns (string memory);
}

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



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        // solhint-disable-next-line avoid-low-level-calls, avoid-call-value
        (bool success, ) = recipient.call{ value: amount }("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain`call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
      return functionCall(target, data, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        require(isContract(target), "Address: call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.call{ value: value }(data);
        return _verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) {
        require(isContract(target), "Address: static call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.staticcall(data);
        return _verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
        require(isContract(target), "Address: delegate call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return _verifyCallResult(success, returndata, errorMessage);
    }

    function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) {
        if (success) {
            return returndata;
        } else {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly

                // solhint-disable-next-line no-inline-assembly
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

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



// SPDX-License-Identifier: MIT

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) {
        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
        return msg.data;
    }
}

// File: @openzeppelin/contracts/utils/introspection/ERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 *
 * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
 */
abstract contract ERC165 is IERC165 {
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: @openzeppelin/contracts/token/ERC20/ERC20.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 * For a generic mechanism see {ERC20PresetMinterPauser}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * We have followed general OpenZeppelin guidelines: functions revert instead
 * of returning `false` on failure. This behavior is nonetheless conventional
 * and does not conflict with the expectations of ERC20 applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 *
 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
 * functions have been added to mitigate the well-known issues around setting
 * allowances. See {IERC20-approve}.
 */
contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping (address => uint256) private _balances;

    mapping (address => mapping (address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * The defaut value of {decimals} is 18. To select a different value for
     * {decimals} you should overload it.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    constructor (string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5,05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the value {ERC20} uses, unless this function is
     * overridden;
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `recipient` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(_msgSender(), recipient, amount);
        return true;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        _approve(_msgSender(), spender, amount);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * Requirements:
     *
     * - `sender` and `recipient` cannot be the zero address.
     * - `sender` must have a balance of at least `amount`.
     * - the caller must have allowance for ``sender``'s tokens of at least
     * `amount`.
     */
    function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(sender, recipient, amount);

        uint256 currentAllowance = _allowances[sender][_msgSender()];
        require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
        _approve(sender, _msgSender(), currentAllowance - amount);

        return true;
    }

    /**
     * @dev Atomically increases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
        return true;
    }

    /**
     * @dev Atomically decreases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `spender` must have allowance for the caller of at least
     * `subtractedValue`.
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        uint256 currentAllowance = _allowances[_msgSender()][spender];
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        _approve(_msgSender(), spender, currentAllowance - subtractedValue);

        return true;
    }

    /**
     * @dev Moves tokens `amount` from `sender` to `recipient`.
     *
     * This is internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - `sender` cannot be the zero address.
     * - `recipient` cannot be the zero address.
     * - `sender` must have a balance of at least `amount`.
     */
    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(sender, recipient, amount);

        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[sender] = senderBalance - amount;
        _balances[recipient] += amount;

        emit Transfer(sender, recipient, amount);
    }

    /** @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, reducing the
     * total supply.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens.
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        _balances[account] = accountBalance - amount;
        _totalSupply -= amount;

        emit Transfer(account, address(0), amount);
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be to transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}

// File: contracts/interfaces/IHedge.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

interface IHedge {
    function init(address _ammAddress) external;

    function underlying() external returns (address);

    function shortAsset() external returns (address);

    function buy(uint256 _underlyingAmount, uint256 _amountInMaximum) external returns (uint256 amountIn);

    function sell(uint256 _underlyingAmount, uint256 _amountOutMinimum) external returns (uint256 amountOut);

    function getShortPrice(uint256 _size) external view returns (uint256);

    function buyShort(uint256 _size, uint256 _amountToPay) external returns (uint256 amountIn);

    function sellShort(uint256 _size, uint256 amountToReceive) external returns (uint256 amountOut);

    function getValueQuote(int256 _position) external view returns (uint256);
}

// File: contracts/lib/PredyMath.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

library PredyMath {
    function max(uint128 a, uint128 b) internal pure returns (uint128) {
        return a > b ? a : b;
    }

    function min(uint128 a, uint128 b) internal pure returns (uint128) {
        return a > b ? b : a;
    }

    function abs(int128 x) internal pure returns (uint128) {
        return uint128(x >= 0 ? x : -x);
    }

    function mulDiv(
        uint256 _x,
        uint256 _y,
        uint256 _d,
        bool _roundUp
    ) internal pure returns (uint128) {
        uint256 tailing;
        if (_roundUp) {
            uint256 remainer = (_x * _y) % _d;
            if (remainer > 0) {
                tailing = 1;
            }
        }

        uint256 result = (_x * _y) / _d + tailing;

        return SafeCast.toUint128(result);
    }

    function scale(
        uint256 _a,
        uint256 _from,
        uint256 _to
    ) internal pure returns (uint256) {
        if (_from > _to) {
            return _a / 10**(_from - _to);
        } else if (_from < _to) {
            return _a * 10**(_to - _from);
        } else {
            return _a;
        }
    }

    function toInt128(uint256 _a) internal pure returns (int128) {
        return SafeCast.toInt128(SafeCast.toInt256(_a));
    }
}

// File: contracts/lib/OptionLib.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./PriceCalculator.sol";
import "../interfaces/IAave.sol";
import "../interfaces/IHedge.sol";
import "../interfaces/IOptionVault.sol";
import "../interfaces/IWETH.sol";
import "../interfaces/IFeePool.sol";
import "./PredyMath.sol";

/**
 * @title OptionLib
 */
library OptionLib {
    using PredyMath for uint128;

    struct TokenContracts {
        // collateral token
        address collateral;
        // underlying token
        address underlying;
    }

    /// @dev option information
    struct OptionInfo {
        address aggregator;
        TokenContracts tokens;
        // counter of serieses
        uint128 seriesCount;
        uint128 expiryCount;
        uint128 expiredCount;
        // expiration => seriesId => option series
        mapping(uint256 => IOptionVault.Expiration) expiries;
        //
        mapping(uint256 => IOptionVault.OptionSeries) serieses;
        // counter of vaults
        uint256 vaultCount;
        // accountId => IOptionVault.Account
        uint128 totalDepositedToLendingPool;
        mapping(uint256 => IOptionVault.Account) accounts;
        // config
        mapping(uint8 => uint128) configs;
        //
        LendingPool lendingPool;
    }

    uint8 public constant MM_RATIO = 1;
    uint8 public constant IM_RATIO = 2;
    uint8 public constant CALL_SAFE_RATIO = 3;
    uint8 public constant PUT_SAFE_RATIO = 4;
    uint8 public constant SLIPPAGE_TOLERANCE = 5;
    uint8 public constant BASE_LIQ_REWARD = 8;
    uint8 public constant REWARD_PER_SIZE_RATIO = 9;

    /// @dev minimum vault id.
    /// trader's vault id is larger than MIN_VAULT_ID
    uint128 public constant MIN_VAULT_ID = 100;

    uint256 constant MAX_UINT256 = 2**256 - 1;

    modifier existsExpiry(uint256 _expiryId, uint128 _expiryCount) {
        require(_expiryId > 0 && _expiryId < _expiryCount, "OptionLib: expiry not found");
        _;
    }

    modifier existsSeries(uint256 _seriesId, uint128 _seriesCount) {
        require(_seriesId > 0 && _seriesId < _seriesCount, "OptionLib: series not found");
        _;
    }

    /**
     * @notice initialize OptionInfo
     */
    function init(
        OptionInfo storage _optionInfo,
        address _collateral,
        address _underlying,
        address _lendingPool
    ) external {
        // initialize states
        _optionInfo.expiredCount = 0;
        _optionInfo.expiryCount = 1;
        _optionInfo.seriesCount = 1;
        _optionInfo.vaultCount = OptionLib.MIN_VAULT_ID;

        // initialize configs
        // 10%
        _optionInfo.configs[MM_RATIO] = 100;
        // 20%
        _optionInfo.configs[IM_RATIO] = 200;
        // 120%
        _optionInfo.configs[CALL_SAFE_RATIO] = 1200;
        // 120%
        _optionInfo.configs[PUT_SAFE_RATIO] = 1200;
        // 0.5%
        _optionInfo.configs[SLIPPAGE_TOLERANCE] = 50;
        // $100
        _optionInfo.configs[BASE_LIQ_REWARD] = 100 * 1e6;
        // 9%
        _optionInfo.configs[REWARD_PER_SIZE_RATIO] = 90;

        // set contract addresses
        _optionInfo.tokens.collateral = _collateral;
        _optionInfo.tokens.underlying = _underlying;
        _optionInfo.lendingPool = LendingPool(_lendingPool);
    }

    function setIV(
        OptionInfo storage _optionInfo,
        uint256 _seriesId,
        uint128 _iv
    ) external {
        _optionInfo.serieses[_seriesId].iv = uint64(_iv);
    }

    /**
     * @notice create new option series
     * the option series must have later expiry than existence serieses
     */
    function createExpiry(OptionInfo storage _optionInfo, uint64 _expiry) external returns (uint128 expiryId) {
        // check expiry is greater than or equal to last created
        uint64 lastExpiry;
        if (_optionInfo.expiryCount > 0) {
            lastExpiry = _optionInfo.expiries[_optionInfo.expiryCount - 1].expiry;
        }

        require(_expiry >= block.timestamp, "OptionLib: expiry must be greater than now");
        require(_expiry >= lastExpiry, "OptionLib: expiry must be greater than or equal to last created");
        require(_expiry % 1 hours == 0, "OptionLib: expiry must be formatted");

        // create series
        expiryId = _optionInfo.expiryCount;
        _optionInfo.expiries[expiryId].expiryId = expiryId;
        _optionInfo.expiries[expiryId].expiry = _expiry;

        _optionInfo.expiryCount += 1;
    }

    function createSeries(
        OptionInfo storage _optionInfo,
        uint128 _expiryId,
        uint64 _strike,
        bool _isPut,
        uint64 _iv
    ) external returns (uint256 seriesId) {
        require(_strike > 0 && _strike < 1e16, "OptionLib: strike must be greater than 0 and less than $100M");
        require(_iv > 0 && _iv < 1000 * 1e6, "OptionLib: iv must be greater than 0 and less than 1000%");

        seriesId = _optionInfo.seriesCount;

        // create series
        _optionInfo.serieses[seriesId] = IOptionVault.OptionSeries(_strike, _isPut, _iv, _expiryId);
        _optionInfo.expiries[_expiryId].seriesIds.push(seriesId);

        _optionInfo.seriesCount += 1;
    }

    function createAccount(OptionInfo storage _optionInfo, address owner) external returns (uint256) {
        uint256 id = _optionInfo.vaultCount;

        IOptionVault.Account storage account = _optionInfo.accounts[id];
        account.owner = owner;
        account.settledCount = _optionInfo.expiredCount;

        _optionInfo.vaultCount += 1;

        return id;
    }

    function deposit(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _amount
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) {
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[_expiryId];

        require(!vault.isSettled, "Vault already settled");

        increaseCollateral(vault, _amount);
    }

    function withdraw(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _amount,
        uint128 _spot
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) {
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[_expiryId];

        checkOptionSeriesIsLive(_optionInfo.expiries[_expiryId]);

        decreaseCollateral(vault, _amount);

        checkCollateral(_optionInfo, _accountId, _expiryId, _spot);
    }

    function withdrawUnrequiredCollateral(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot,
        uint128 _cRatio,
        bool _isPool
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) returns (uint128) {
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[_expiryId];

        checkOptionSeriesIsLive(_optionInfo.expiries[_expiryId]);

        uint128 collateralValue = getCollateralValue(_optionInfo, _accountId, _expiryId, _spot);

        uint128 requiredCollateral = (1e6 *
            getRequiredMargin(
                _optionInfo,
                _accountId,
                _expiryId,
                _spot,
                _isPool ? IOptionVault.MarginLevel.Safe : IOptionVault.MarginLevel.Initial
            )) / _cRatio;

        if (collateralValue > requiredCollateral) {
            return decreaseCollateral(vault, collateralValue - requiredCollateral);
        } else {
            return 0;
        }
    }

    function write(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _spot
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.OptionSeries storage series = _optionInfo.serieses[_seriesId];

        checkOptionSeriesIsLive(_optionInfo.expiries[series.expiryId]);

        account.vaults[series.expiryId].shorts[_seriesId] += _amount;

        checkCollateral(_optionInfo, _accountId, series.expiryId, _spot);
    }

    function unlock(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) {
        IOptionVault.OptionSeries storage series = _optionInfo.serieses[_seriesId];

        _optionInfo.accounts[_accountId].vaults[series.expiryId].shorts[_seriesId] -= _amount;
    }

    function depositAndWrite(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _cRatio,
        uint128 _spot,
        bool _isPool
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) returns (uint256 expiryId, uint128 collateral) {
        expiryId = _optionInfo.serieses[_seriesId].expiryId;

        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[expiryId];

        checkOptionSeriesIsLive(_optionInfo.expiries[expiryId]);

        account.vaults[expiryId].shorts[_seriesId] += _amount;

        collateral = getCollateralForASeries(_optionInfo, expiryId, _seriesId, _spot, _amount, _cRatio, _isPool);

        increaseCollateral(vault, collateral);
    }

    function addLong(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) returns (uint128) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];

        account.vaults[_expiryId].longs[_seriesId] += _amount;

        return account.vaults[_expiryId].longs[_seriesId];
    }

    function removeLong(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _amount
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];

        account.vaults[_expiryId].longs[_seriesId] -= _amount;
    }

    /**
     * @notice liquidate a vault
     * the amount that liquidator can burn is '(BASE_REWRD + MM - C) / (mm - premium - REWARD_RATIO * (St or K))'
     * where C = vault.collateral
     * , MM current maintenance margin
     * and mm is maintenance margin per size
     */
    function liquidate(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _spot
    ) external existsSeries(_seriesId, _optionInfo.seriesCount) returns (uint128) {
        IOptionVault.OptionSeries memory option = _optionInfo.serieses[_seriesId];
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[option.expiryId];

        uint128 shortAmount = vault.shorts[_seriesId];

        require(shortAmount >= _amount, "OptionLib: amount exceeds vault size");

        uint128 collateralValue = getCollateralValue(_optionInfo, _accountId, option.expiryId, _spot);

        uint128 limit;
        {
            uint128 maintenanceMargin = getRequiredMargin(
                _optionInfo,
                _accountId,
                option.expiryId,
                _spot,
                IOptionVault.MarginLevel.Maintenance
            );
            require(collateralValue < maintenanceMargin, "OptionLib: collateral must be less than MM");

            limit = calLiquidatableAmount(
                _optionInfo,
                maintenanceMargin,
                collateralValue,
                option.isPut ? option.strike : _spot
            );
        }

        require(limit >= _amount, "OptionLib: amount exceeds liquidatable limit");

        vault.shorts[_seriesId] -= _amount;

        uint128 reward;
        {
            uint128 maintenanceMargin = getRequiredMargin(
                _optionInfo,
                _accountId,
                option.expiryId,
                _spot,
                IOptionVault.MarginLevel.Maintenance
            );

            require(collateralValue >= maintenanceMargin, "OptionLib: margin must be safe");
            reward = collateralValue - maintenanceMargin;
        }

        return decreaseCollateral(vault, reward);
    }

    /**
     * @notice calculate profit of some amount of option contracts
     */
    function claimProfit(
        OptionInfo storage _optionInfo,
        uint256 _seriesId,
        uint128 _amount,
        uint128 _price
    ) external view existsSeries(_seriesId, _optionInfo.seriesCount) returns (uint128) {
        uint128 payout = calculatePayout(
            _amount,
            _price,
            _optionInfo.serieses[_seriesId].strike,
            _optionInfo.serieses[_seriesId].isPut,
            false
        );

        return payout;
    }

    /**
     * @notice fix payout of a vault and remove the payout from vault's collaterals
     * @return settledAmount the amount that the vault owner can redeem
     */
    function settle(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _price
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) returns (uint128 settledAmount) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[_expiryId];

        uint128 payout = getTotalPayout(_optionInfo, _accountId, _expiryId, _price, true);

        require(!vault.isSettled, "OptionLib: vault already settled");

        require(vault.hedgePosition == 0, "OptionLib: hedge position must be neutral");

        // all collaterals in Aave must be redeemed before settlement
        require(vault.shortLiquidity == 0, "OptionLib: all collaterals must be withdrawn");

        require(account.settledCount < _expiryId, "OptionLib: vault already settled");

        for (uint256 i = account.settledCount + 1; i < _expiryId; i++) {
            uint128 skippedCollateral = account.vaults[i].collateral;
            require(skippedCollateral == 0, "OptionLib: can not skip expiry");
        }

        vault.isSettled = true;
        account.settledCount = _expiryId;

        updateExpiredCount(_optionInfo, _expiryId);

        decreaseCollateral(vault, payout);

        settledAmount = vault.collateral;
    }

    /*
     * addUnderlyingLong and addUnderlyingShort are functions to achieve delta neutral.
     * vault's net delta is calculated as vaultDelta, and the protocol wanna make (vaultDelta + vault.hedgePosition) zero.
     * hedgePosition exactly represents how many underlying asset the vault has,
     * and negative hedgePosition means short position of underlying asset.
     */

    /**
     * @notice receive underlying asset and send collateral asset to sender
     * @param _vaultDelta net delta of the vault
     * @param _underlyingAmountE8 amount of underlying asset scaled by 1e8
     */
    function addUnderlyingLong(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot,
        int256 _vaultDelta,
        uint256 _underlyingAmountE8,
        uint256 _collateralAmount
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) returns (int256) {
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[_expiryId];
        ERC20 underlying = ERC20(_optionInfo.tokens.underlying);

        uint256 decimals = underlying.decimals();

        require(_vaultDelta + vault.hedgePosition < 0, "OptionLib: net delta must be negative");

        require(
            -(_vaultDelta + vault.hedgePosition) >= int256(_underlyingAmountE8),
            "OptionLib: underlying amount is too large"
        );

        if (-(_vaultDelta + vault.hedgePosition) < int256(_underlyingAmountE8)) {
            _underlyingAmountE8 = uint256(-(_vaultDelta + vault.hedgePosition));
        }

        require(
            (PredyMath.scale(_spot * _underlyingAmountE8, 16, 6) * (10000 + _optionInfo.configs[SLIPPAGE_TOLERANCE])) /
                10000 >=
                _collateralAmount,
            "OptionLib: collateral amount is too large"
        );

        uint256 uAmount = PredyMath.scale(_underlyingAmountE8, 8, decimals);

        int256 hedgePosition = vault.hedgePosition;

        vault.hedgePosition += int256(_underlyingAmountE8);

        underlying.transferFrom(msg.sender, address(this), uAmount);

        if (hedgePosition < -int256(_underlyingAmountE8)) {
            repayUnderlyingInternal(_optionInfo, vault, _spot, uAmount, uint256(-hedgePosition) - _underlyingAmountE8);
        } else if (hedgePosition < 0) {
            repayUnderlyingInternal(
                _optionInfo,
                vault,
                _spot,
                PredyMath.scale(uint256(-hedgePosition), 8, decimals),
                0
            );
        }

        require(vault.collateral >= uint128(_collateralAmount), "OptionLib: no enough collateral");

        decreaseCollateral(vault, uint128(_collateralAmount));

        IERC20(_optionInfo.tokens.collateral).transfer(msg.sender, _collateralAmount);

        return vault.hedgePosition;
    }

    /**
     * @notice receive collateral asset and send underlying asset to sender
     * @param _vaultDelta net delta of the vault
     * @param _underlyingAmountE8 amount of underlying asset scaled by 1e8
     */
    function addUnderlyingShort(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot,
        int256 _vaultDelta,
        uint256 _underlyingAmountE8,
        uint256 _collateralAmount
    ) external existsExpiry(_expiryId, _optionInfo.expiryCount) returns (int256) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[_expiryId];
        ERC20 underlying = ERC20(_optionInfo.tokens.underlying);

        uint256 decimals = underlying.decimals();

        require(_vaultDelta + vault.hedgePosition > 0, "OptionLib: net delta must be positive");

        require(
            (_vaultDelta + vault.hedgePosition) >= int256(_underlyingAmountE8),
            "OptionLib: underlying amount is too large"
        );

        require(
            (PredyMath.scale(_spot * _underlyingAmountE8, 16, 6) * (10000 - _optionInfo.configs[SLIPPAGE_TOLERANCE])) /
                10000 <=
                _collateralAmount,
            "OptionLib: collateral amount is too small"
        );

        uint256 uAmount = PredyMath.scale(_underlyingAmountE8, 8, decimals);

        increaseCollateral(vault, uint128(_collateralAmount));

        IERC20(_optionInfo.tokens.collateral).transferFrom(msg.sender, address(this), _collateralAmount);

        if (vault.hedgePosition <= 0) {
            borrowUnderlyingInternal(_optionInfo, vault, _spot, uAmount);
        } else if (vault.hedgePosition < int256(_underlyingAmountE8)) {
            borrowUnderlyingInternal(
                _optionInfo,
                vault,
                _spot,
                uAmount - PredyMath.scale(uint256(vault.hedgePosition), 8, decimals)
            );
        }

        vault.hedgePosition -= int256(_underlyingAmountE8);

        underlying.transfer(msg.sender, uAmount);

        return vault.hedgePosition;
    }

    /**
     * @notice redeem collateral from LendingPool
     */
    function redeemCollateralFromLendingPool(
        OptionInfo storage _optionInfo,
        uint128 _repayAmount,
        uint128 _price,
        address _caller,
        address _feePool
    ) external {
        // there are no live option serieses
        require(_optionInfo.expiryCount == _optionInfo.expiredCount + 1);
        // check total short liquidity is 0
        require(_optionInfo.totalDepositedToLendingPool == 0);

        IERC20(_optionInfo.tokens.underlying).transferFrom(_caller, address(this), _repayAmount);

        IERC20(_optionInfo.tokens.underlying).approve(address(_optionInfo.lendingPool), _repayAmount);
        uint128 repaidAmount = repayBorrow(_optionInfo, MAX_UINT256);

        // redeem underlying tokens
        uint128 withdrawnAmount = uint128(
            _optionInfo.lendingPool.withdraw(_optionInfo.tokens.collateral, MAX_UINT256, address(this))
        );

        uint128 reward = (_price * repaidAmount) / 1e20;
        reward = (reward * (10000 + _optionInfo.configs[SLIPPAGE_TOLERANCE])) / 10000;

        if (reward < withdrawnAmount) {
            uint128 rewardForFeePool = withdrawnAmount - reward;

            // send USDC to fee pool
            IFeePool feePool = IFeePool(_feePool);

            IERC20(_optionInfo.tokens.collateral).approve(address(feePool), rewardForFeePool);
            feePool.sendProfitERC20(address(this), rewardForFeePool);
        } else {
            reward = withdrawnAmount;
        }

        // send USDC and WETH to caller
        IERC20(_optionInfo.tokens.collateral).transfer(_caller, reward);
        IERC20(_optionInfo.tokens.underlying).transfer(_caller, _repayAmount - repaidAmount);
    }

    function setConfig(
        OptionInfo storage _optionInfo,
        uint8 _key,
        uint128 _value
    ) external {
        _optionInfo.configs[_key] = _value;
    }

    //////////////////////
    // Getter Functions //
    //////////////////////

    function getLiveOptionSerieses(OptionInfo storage _optionInfo)
        external
        view
        returns (IOptionVault.Expiration[] memory)
    {
        IOptionVault.Expiration[] memory expirations = new IOptionVault.Expiration[](
            _optionInfo.expiryCount - _optionInfo.expiredCount - 1
        );

        for (uint128 i = _optionInfo.expiredCount + 1; i < _optionInfo.expiryCount; i++) {
            expirations[i - _optionInfo.expiredCount - 1] = IOptionVault.Expiration(
                i,
                _optionInfo.expiries[i].expiry,
                _optionInfo.expiries[i].seriesIds
            );
        }

        return expirations;
    }

    function getOptionSeriesView(OptionInfo storage _optionInfo, uint256 _seriesId)
        external
        view
        returns (IOptionVault.OptionSeriesView memory)
    {
        IOptionVault.OptionSeries memory optionSeries = _optionInfo.serieses[_seriesId];
        IOptionVault.Expiration memory expiration = _optionInfo.expiries[optionSeries.expiryId];

        return
            IOptionVault.OptionSeriesView(
                optionSeries.expiryId,
                _seriesId,
                expiration.expiry,
                OptionLib.getMaturity(expiration.expiry),
                optionSeries.strike,
                optionSeries.isPut,
                optionSeries.iv
            );
    }

    function getCollateralValueQuote(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint128 price
    ) external view returns (uint128 total) {
        for (uint256 i = _optionInfo.accounts[_accountId].settledCount + 1; i < _optionInfo.seriesCount; i++) {
            total += getCollateralValue(_optionInfo, _accountId, i, price);
        }
    }

    /**
     * @notice get position size
     * @param _accountId vault id
     * @param _seriesId option series id
     * @return (short size, long size)
     */
    function getPositionSize(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId
    ) external view returns (uint128, uint128) {
        IOptionVault.OptionSeries memory series = _optionInfo.serieses[_seriesId];
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[series.expiryId];

        return (vault.shorts[_seriesId], vault.longs[_seriesId]);
    }

    function getVaultOwner(OptionInfo storage _optionInfo, uint256 _accountId) external view returns (address) {
        return _optionInfo.accounts[_accountId].owner;
    }

    /**
     * @notice get the amount that can be liquidated
     */
    function getLiquidatableAmount(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _seriesId,
        uint128 _spot
    ) external view returns (uint128) {
        IOptionVault.OptionSeries memory option = _optionInfo.serieses[_seriesId];
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[option.expiryId];

        uint128 maintenanceMargin = getRequiredMargin(
            _optionInfo,
            _accountId,
            option.expiryId,
            _spot,
            IOptionVault.MarginLevel.Maintenance
        );

        uint128 collateralValue = getCollateralValue(_optionInfo, _accountId, option.expiryId, _spot);

        if (collateralValue >= maintenanceMargin) {
            return 0;
        }

        uint128 liquidatableAmount = calLiquidatableAmount(
            _optionInfo,
            maintenanceMargin,
            collateralValue,
            option.isPut ? option.strike : _spot
        );

        if (vault.shorts[_seriesId] < liquidatableAmount) {
            return vault.shorts[_seriesId];
        }

        return liquidatableAmount;
    }

    int256 internal constant SQRT_YEAR_E8 = 5615.69229926 * 10**8;

    /**
     * @notice calculate vault's net delta
     * @param _optionInfo pool info
     * @param _accountId vault id to calculate net delta
     * @param _spot spot price
     * @return tickDelta vault's net delta scaled by 1e8
     */
    function calculateVaultDelta(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot
    ) external view returns (int256 tickDelta) {
        IOptionVault.Vault storage vault = _optionInfo.accounts[_accountId].vaults[_expiryId];

        // calculate serieses that is not expired
        IOptionVault.Expiration memory expiration = _optionInfo.expiries[_expiryId];

        // option serieses before maturity are included in the calculation.
        if (expiration.expiry <= block.timestamp) {
            return 0;
        }

        // uint64 maturity = getMaturity(expiration.expiry);
        int256 sqrtMaturity = PriceCalculator.getSqrtMaturity(getMaturity(expiration.expiry));

        for (uint256 j = 0; j < expiration.seriesIds.length; j++) {
            uint256 seriesId = expiration.seriesIds[j];

            int128 position = vault.longs[seriesId].toInt128() - vault.shorts[seriesId].toInt128();
            tickDelta += calculateDelta(sqrtMaturity, _optionInfo.serieses[seriesId], position, _spot);
        }
        return tickDelta;
    }

    ///////////////////////
    // Private Functions //
    ///////////////////////

    function increaseCollateral(IOptionVault.Vault storage _vault, uint128 _amount) internal {
        _vault.collateral += _amount;
    }

    function decreaseCollateral(IOptionVault.Vault storage _vault, uint128 _amount) internal returns (uint128) {
        if (_vault.collateral >= _amount) {
            _vault.collateral -= _amount;
            return _amount;
        } else {
            uint128 a = _vault.collateral;
            _vault.collateral = 0;
            return a;
        }
    }

    /**
     * @notice get USD value of collateral
     */
    function getCollateralValue(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot
    ) internal view returns (uint128) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[_expiryId];

        int256 hedgedValue = vault.shortLiquidity.toInt128() + (_spot.toInt128() * vault.hedgePosition) / 1e10;

        return uint128(uint256(hedgedValue)) + vault.collateral;
    }

    /**
     * @notice get the required margin of a vault
     */
    function getRequiredMargin(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot,
        IOptionVault.MarginLevel _marginLevel
    ) public view returns (uint128 requiredMargin) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[_expiryId];
        IOptionVault.Expiration storage expiration = _optionInfo.expiries[_expiryId];

        if (vault.isSettled) {
            return 0;
        }

        for (uint256 i = 0; i < expiration.seriesIds.length; i++) {
            uint256 seriesId = expiration.seriesIds[i];
            uint128 shortAmount = vault.shorts[seriesId];
            uint128 longAmount = vault.longs[seriesId];

            if (shortAmount == 0 && longAmount == 0) {
                continue;
            }

            IOptionVault.OptionSeriesParams memory seriesParams = getOptionSeriesParams(
                _optionInfo.serieses[seriesId],
                expiration.expiry
            );

            if (_marginLevel == IOptionVault.MarginLevel.Safe) {
                requiredMargin += calMargin(
                    _optionInfo,
                    shortAmount.toInt128() - longAmount.toInt128(),
                    _spot,
                    seriesParams,
                    _marginLevel
                );
            } else {
                requiredMargin += calMargin(_optionInfo, shortAmount.toInt128(), _spot, seriesParams, _marginLevel);
            }
        }
    }

    function getRequiredMarginForASeries(
        OptionInfo storage _optionInfo,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _spot,
        int128 _amount,
        IOptionVault.MarginLevel _marginLevel
    ) public view returns (uint128) {
        uint64 expiry = _optionInfo.expiries[_expiryId].expiry;

        IOptionVault.OptionSeries memory series = _optionInfo.serieses[_seriesId];

        return calMargin(_optionInfo, _amount, _spot, getOptionSeriesParams(series, expiry), _marginLevel);
    }

    /**
     * @notice get the total payout of a vault
     */
    function getTotalPayout(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot,
        bool _roundUp
    ) public view returns (uint128 totalPayout) {
        IOptionVault.Account storage account = _optionInfo.accounts[_accountId];
        IOptionVault.Vault storage vault = account.vaults[_expiryId];

        IOptionVault.Expiration memory expiration = _optionInfo.expiries[_expiryId];

        for (uint256 i = 0; i < expiration.seriesIds.length; i++) {
            uint256 seriesId = expiration.seriesIds[i];

            if (vault.shorts[seriesId] == 0) {
                continue;
            }

            totalPayout += calculatePayout(
                vault.shorts[seriesId],
                _spot,
                _optionInfo.serieses[seriesId].strike,
                _optionInfo.serieses[seriesId].isPut,
                _roundUp
            );
        }
    }

    /**
     * @notice calculate liquidatable amount
     * Calculate the liquidatable size so that the collateral of the size's premium + BASE_REWARD remains in vault
     */
    function calLiquidatableAmount(
        OptionInfo storage _optionInfo,
        uint128 _maintenanceMargin,
        uint128 _collateralValue,
        uint128 _spotOrStrike
    ) internal view returns (uint128 limit) {
        // maintenance margin - premium
        uint128 rewardDiffPerSize = (_spotOrStrike * _optionInfo.configs[REWARD_PER_SIZE_RATIO]) / (1000);

        return
            (1e10 * (_optionInfo.configs[BASE_LIQ_REWARD] + _maintenanceMargin - _collateralValue)) / rewardDiffPerSize;
    }

    /**
     * @notice get maturity
     */
    function getMaturity(uint64 _expiry) internal view returns (uint64 maturity) {
        maturity = _expiry > block.timestamp ? _expiry - uint64(block.timestamp) : 0;
    }

    function getOptionSeriesParams(IOptionVault.OptionSeries memory _series, uint64 _expiry)
        internal
        view
        returns (IOptionVault.OptionSeriesParams memory)
    {
        uint64 maturity = getMaturity(_expiry);

        return IOptionVault.OptionSeriesParams(_series.expiryId, maturity, _series.strike, _series.isPut, _series.iv);
    }

    function checkOptionSeriesIsLive(IOptionVault.Expiration memory _expiration) internal view {
        require(_expiration.expiry > block.timestamp, "OptionLib: option series has been expired");
    }

    function checkCollateral(
        OptionInfo storage _optionInfo,
        uint256 _accountId,
        uint256 _expiryId,
        uint128 _spot
    ) internal view {
        require(
            getCollateralValue(_optionInfo, _accountId, _expiryId, _spot) >=
                getRequiredMargin(_optionInfo, _accountId, _expiryId, _spot, IOptionVault.MarginLevel.Initial),
            "OptionLib: collateral is not enough"
        );
    }

    function getCollateralForASeries(
        OptionInfo storage _optionInfo,
        uint256 _expiryId,
        uint256 _seriesId,
        uint128 _spot,
        uint128 _amount,
        uint128 _cRatio,
        bool _isPool
    ) internal view returns (uint128 collateral) {
        uint128 requiredMargin = getRequiredMarginForASeries(
            _optionInfo,
            _expiryId,
            _seriesId,
            _spot,
            _amount.toInt128(),
            _isPool ? IOptionVault.MarginLevel.Safe : IOptionVault.MarginLevel.Initial
        );

        collateral = (1e6 * requiredMargin) / _cRatio;
    }

    /**
     * @notice calculate required margin
     * there are 3 margin levels, Maintenance Margin, Initial Margin and Safe Margin.
     * Maintenance Margin: premium + mmRatio * spot
     * Initial Margin: premium + imRatio * spot
     * Safe Margin:
     *   120% of spot for short call
     *   200% of min(strike, spot) for short put
     *   200% of min(strike, spot) for long call
     *   120% of spot for long put
     */
    function calMargin(
        OptionInfo storage _optionInfo,
        int128 _u,
        uint128 _spot,
        IOptionVault.OptionSeriesParams memory _series,
        IOptionVault.MarginLevel _marginLevel
    ) internal view returns (uint128) {
        if (_marginLevel == IOptionVault.MarginLevel.Maintenance || _marginLevel == IOptionVault.MarginLevel.Initial) {
            uint128 size = uint128(_u);
            // calculate as ATM if option is OTM

            uint256 p = PriceCalculator.calculatePrice(
                _spot,
                _series.strike,
                _series.maturity,
                _series.iv,
                _series.isPut
            );

            if (_marginLevel == IOptionVault.MarginLevel.Initial) {
                // initial margin
                if (_series.isPut) {
                    p += (_series.strike * _optionInfo.configs[IM_RATIO]) / (1000);
                } else {
                    p += (_spot * _optionInfo.configs[IM_RATIO]) / (1000);
                }
            } else {
                // maintenance margin
                if (_series.isPut) {
                    p += (_series.strike * _optionInfo.configs[MM_RATIO]) / (1000);
                } else {
                    p += (_spot * _optionInfo.configs[MM_RATIO]) / (1000);
                }
            }

            return uint128(size * p) / 1e10;
        } else if (_marginLevel == IOptionVault.MarginLevel.Safe) {
            bool collateralForShort;
            uint128 size;

            if (_u > 0) {
                size = uint128(_u);
                collateralForShort = _series.isPut;
            } else {
                size = uint128(-_u);
                collateralForShort = !_series.isPut;
            }

            if (collateralForShort) {
                return
                    (size * PredyMath.min(_series.strike, _spot) * _optionInfo.configs[PUT_SAFE_RATIO]) / (1e10 * 1000);
            } else {
                return (size * _spot * _optionInfo.configs[CALL_SAFE_RATIO]) / (1e10 * 1000);
            }
        }

        return 0;
    }

    /**
     * @notice calculate payout
     * put: amount * max(K - S, 0)
     * call: amount * max(S - K, 0)
     */
    function calculatePayout(
        uint128 _u,
        uint128 _spot,
        uint128 _strike,
        bool _isPut,
        bool _isRoundUp
    ) internal pure returns (uint128) {
        uint128 r;
        if (_isPut && _strike > _spot) {
            r = _strike - _spot;
        } else if (!_isPut && _strike < _spot) {
            r = _spot - _strike;
        } else {
            return 0;
        }
        return PredyMath.mulDiv(_u, r, 1e10, _isRoundUp);
    }

    function updateExpiredCount(OptionInfo storage _optionInfo, uint256 _expiryId) internal {
        IOptionVault.Expiration storage expiration = _optionInfo.expiries[_expiryId];

        require(expiration.expiry < block.timestamp, "expiry must have been passed");

        if (_optionInfo.expiredCount < _expiryId) {
            _optionInfo.expiredCount = uint128(_expiryId);
        }
    }

    function calculateDelta(
        int256 _maturity,
        IOptionVault.OptionSeries memory _option,
        int256 _position,
        uint128 _spot
    ) internal pure returns (int256 delta) {
        return
            (_position * PriceCalculator.calculateDelta(_spot, _option.strike, _maturity, _option.iv, _option.isPut)) /
            int256(1e8);
    }

    /**
     * @notice deposits USDC and borrows underlying asset from compound
     */
    function borrowUnderlyingInternal(
        OptionInfo storage _optionInfo,
        IOptionVault.Vault storage vault,
        uint128 _spot,
        uint256 _underlyingAmount
    ) internal {
        ERC20 underlying = ERC20(_optionInfo.tokens.underlying);

        uint256 decimals = underlying.decimals();

        uint128 depositCollateral = uint128(PredyMath.scale(_underlyingAmount * _spot * 2.0, decimals + 8, 6));

        // deposit USDC to compound
        IERC20(_optionInfo.tokens.collateral).approve(address(_optionInfo.lendingPool), depositCollateral);
        _optionInfo.lendingPool.deposit(_optionInfo.tokens.collateral, depositCollateral, address(this), 0);

        // borrow underling
        borrow(_optionInfo, _underlyingAmount);

        require(vault.collateral >= depositCollateral, "OptionLib: no enough collateral");

        depositCollateral = decreaseCollateral(vault, depositCollateral);
        vault.shortLiquidity += depositCollateral;
        _optionInfo.totalDepositedToLendingPool += depositCollateral;
    }

    /**
     * @notice repays underlying asset and withdraws USDC from compound
     * @param _optionInfo option vault object
     * @param _vault the vault repaying underlying asset
     * @param _spot spot price
     * @param _underlyingAmount amount to repay
     * @param _remainingDebt The remaining debt after the repayment, scaled by 1e8.
     */
    function repayUnderlyingInternal(
        OptionInfo storage _optionInfo,
        IOptionVault.Vault storage _vault,
        uint128 _spot,
        uint256 _underlyingAmount,
        uint256 _remainingDebt
    ) internal {
        IERC20(_optionInfo.tokens.underlying).approve(address(_optionInfo.lendingPool), _underlyingAmount);
        repayBorrow(_optionInfo, _underlyingAmount);

        // calculate unrequired collateral
        // if maturity is large, the protocol have to pay borrowing interest, and it can not redeem all shortLiquidity.
        // In this case, repay underling from outside of protocol.
        uint128 redeemCollateralAmount = _vault.shortLiquidity - (uint128(_remainingDebt) * _spot * 2) / 1e10;

        _optionInfo.lendingPool.withdraw(_optionInfo.tokens.collateral, redeemCollateralAmount, address(this));

        increaseCollateral(_vault, redeemCollateralAmount);

        _vault.shortLiquidity -= redeemCollateralAmount;
        _optionInfo.totalDepositedToLendingPool -= redeemCollateralAmount;
    }

    /**
     * @notice borrow underlying asset with Variable type debt
     */
    function borrow(OptionInfo storage _optionInfo, uint256 _amount) internal {
        _optionInfo.lendingPool.borrow(_optionInfo.tokens.underlying, _amount, 2, 0, address(this));
    }

    /**
     * @notice repay borrowing underlying asset with Variable type debt
     */
    function repayBorrow(OptionInfo storage _optionInfo, uint256 _amount) internal returns (uint128) {
        return uint128(_optionInfo.lendingPool.repay(_optionInfo.tokens.underlying, _amount, 2, address(this)));
    }
}

// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, uint256 amount) 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 `amount` 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 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    /**
     * @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);
}

// File: @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC20 standard.
 *
 * _Available since v4.1._
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}

// File: @openzeppelin/contracts/utils/math/SafeCast.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow
 * checks.
 *
 * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
 * easily result in undesired exploitation or bugs, since developers usually
 * assume that overflows raise errors. `SafeCast` restores this intuition by
 * reverting the transaction when such an operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 *
 * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing
 * all math on `uint256` and `int256` and then downcasting.
 */
library SafeCast {
    /**
     * @dev Returns the downcasted uint128 from uint256, reverting on
     * overflow (when the input is greater than largest uint128).
     *
     * Counterpart to Solidity's `uint128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     */
    function toUint128(uint256 value) internal pure returns (uint128) {
        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
        return uint128(value);
    }

    /**
     * @dev Returns the downcasted uint64 from uint256, reverting on
     * overflow (when the input is greater than largest uint64).
     *
     * Counterpart to Solidity's `uint64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     */
    function toUint64(uint256 value) internal pure returns (uint64) {
        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
        return uint64(value);
    }

    /**
     * @dev Returns the downcasted uint32 from uint256, reverting on
     * overflow (when the input is greater than largest uint32).
     *
     * Counterpart to Solidity's `uint32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     */
    function toUint32(uint256 value) internal pure returns (uint32) {
        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
        return uint32(value);
    }

    /**
     * @dev Returns the downcasted uint16 from uint256, reverting on
     * overflow (when the input is greater than largest uint16).
     *
     * Counterpart to Solidity's `uint16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     */
    function toUint16(uint256 value) internal pure returns (uint16) {
        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
        return uint16(value);
    }

    /**
     * @dev Returns the downcasted uint8 from uint256, reverting on
     * overflow (when the input is greater than largest uint8).
     *
     * Counterpart to Solidity's `uint8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits.
     */
    function toUint8(uint256 value) internal pure returns (uint8) {
        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
        return uint8(value);
    }

    /**
     * @dev Converts a signed int256 into an unsigned uint256.
     *
     * Requirements:
     *
     * - input must be greater than or equal to 0.
     */
    function toUint256(int256 value) internal pure returns (uint256) {
        require(value >= 0, "SafeCast: value must be positive");
        return uint256(value);
    }

    /**
     * @dev Returns the downcasted int128 from int256, reverting on
     * overflow (when the input is less than smallest int128 or
     * greater than largest int128).
     *
     * Counterpart to Solidity's `int128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     *
     * _Available since v3.1._
     */
    function toInt128(int256 value) internal pure returns (int128) {
        require(value >= -2**127 && value < 2**127, "SafeCast: value doesn\'t fit in 128 bits");
        return int128(value);
    }

    /**
     * @dev Returns the downcasted int64 from int256, reverting on
     * overflow (when the input is less than smallest int64 or
     * greater than largest int64).
     *
     * Counterpart to Solidity's `int64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     *
     * _Available since v3.1._
     */
    function toInt64(int256 value) internal pure returns (int64) {
        require(value >= -2**63 && value < 2**63, "SafeCast: value doesn\'t fit in 64 bits");
        return int64(value);
    }

    /**
     * @dev Returns the downcasted int32 from int256, reverting on
     * overflow (when the input is less than smallest int32 or
     * greater than largest int32).
     *
     * Counterpart to Solidity's `int32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     *
     * _Available since v3.1._
     */
    function toInt32(int256 value) internal pure returns (int32) {
        require(value >= -2**31 && value < 2**31, "SafeCast: value doesn\'t fit in 32 bits");
        return int32(value);
    }

    /**
     * @dev Returns the downcasted int16 from int256, reverting on
     * overflow (when the input is less than smallest int16 or
     * greater than largest int16).
     *
     * Counterpart to Solidity's `int16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     *
     * _Available since v3.1._
     */
    function toInt16(int256 value) internal pure returns (int16) {
        require(value >= -2**15 && value < 2**15, "SafeCast: value doesn\'t fit in 16 bits");
        return int16(value);
    }

    /**
     * @dev Returns the downcasted int8 from int256, reverting on
     * overflow (when the input is less than smallest int8 or
     * greater than largest int8).
     *
     * Counterpart to Solidity's `int8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits.
     *
     * _Available since v3.1._
     */
    function toInt8(int256 value) internal pure returns (int8) {
        require(value >= -2**7 && value < 2**7, "SafeCast: value doesn\'t fit in 8 bits");
        return int8(value);
    }

    /**
     * @dev Converts an unsigned uint256 into a signed int256.
     *
     * Requirements:
     *
     * - input must be less than or equal to maxInt256.
     */
    function toInt256(uint256 value) internal pure returns (int256) {
        require(value < 2**255, "SafeCast: value doesn't fit in an int256");
        return int256(value);
    }
}

// File: contracts/lib/PriceCalculator.sol



// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.0;

import "./AdvancedMath.sol";

/**
 * @notice Option price calculator using Black-Scholes formula
 */
library PriceCalculator {
    /// @dev sqrt(365 * 86400)
    int256 internal constant SQRT_YEAR_E8 = 5615.69229926 * 10**8;
    /// @dev range size of linear function
    int256 internal constant RANGE = 20 * 1e6;

    struct Parameters {
        // spot price
        int256 spot;
        // strike price
        int256 strike;
        // log(spot / strike)
        int256 logSigE4;
        // sqrt(maturity)
        int256 sqrtMaturity;
    }

    /**
     * @notice calculate option price at a IV point
     * @param _spot spot price scaled 1e8
     * @param _strike strike price scaled 1e8
     * @param _maturity maturity in seconds
     * @param _iv IV
     * @param _isPut option type
     * @return premium per amount
     */
    function calculatePrice(
        uint256 _spot,
        uint256 _strike,
        uint256 _maturity,
        uint256 _iv,
        bool _isPut
    ) external pure returns (uint256 premium) {
        validateParameters(_spot, _strike);
        require(0 < _iv && _iv < 1000 * 1e6, "PriceCalculator: implied volatility must be between 0 and 1000%");

        int256 sqrtMaturity = getSqrtMaturity(_maturity);
        int256 logSigE4;
        {
            int256 spotPerStrikeE4 = int256((_spot * 1e4) / _strike);
            logSigE4 = AdvancedMath.logTaylor(spotPerStrikeE4);
        }

        return
            uint256(
                calOptionPrice(
                    Parameters(int256(_spot), int256(_strike), logSigE4, sqrtMaturity),
                    int256(_iv),
                    _isPut,
                    // calculatePrice function never reverts by delta cut-off
                    0
                )
            );
    }

    /**
     * @notice calculate option price with two IV points
     * @param _spot spot price scaled 1e8
     * @param _strike strike price scaled 1e8
     * @param _maturity maturity in seconds
     * @param _x0 start IV
     * @param _x1 end IV
     * @param _isPut option type
     * @param _minDelta minimum delta. if delta is less than minDelta or greater than (100% - minDelta), calculation reverts
     * @return premium per amount
     */
    function calculatePrice2(
        uint256 _spot,
        uint256 _strike,
        uint256 _maturity,
        uint256 _x0,
        uint256 _x1,
        bool _isPut,
        uint256 _minDelta
    ) external pure returns (uint256 premium) {
        validateParameters(_spot, _strike);
        require(0 < _x0 && _x0 < 10 * 1e8, "PriceCalculator: 0 < x0 < 1000%");
        require(0 < _x1 && _x1 < 10 * 1e8, "PriceCalculator: 0 < x1 < 1000%");
        require(_x0 < _x1, "PriceCalculator: _x0 < _x1");

        int256 sqrtMaturity = getSqrtMaturity(_maturity);

        int256 logSigE4;
        {
            int256 spotPerStrikeE4 = int256((_spot * 1e4) / _strike);
            logSigE4 = AdvancedMath.logTaylor(spotPerStrikeE4);
        }

        return
            uint256(
                calculatePriceOfRanges(
                    Parameters(int256(_spot), int256(_strike), logSigE4, sqrtMaturity),
                    int256(_x0),
                    int256(_x1),
                    _isPut,
                    _minDelta
                )
            );
    }

    /**
     * @notice calculate option's delta
     * @param _spot spot price scaled 1e8
     * @param _strike strike price scaled 1e8
     * @param sqrtMaturity maturity in seconds
     * @param _iv IV
     * @param _isPut option type
     * @return delta
     */
    function calculateDelta(
        uint256 _spot,
        uint256 _strike,
        int256 sqrtMaturity,
        uint256 _iv,
        bool _isPut
    ) external pure returns (int256) {
        validateParameters(_spot, _strike);
        require(0 < _iv && _iv < 1000 * 1e6, "PriceCalculator: implied volatility must be between 0 and 1000%");

        int256 logSigE4;
        {
            int256 spotPerStrikeE4 = int256((_spot * 1e4) / _strike);
            logSigE4 = AdvancedMath.logTaylor(spotPerStrikeE4);
        }
        (int256 d1E4, ) = _calD1D2(logSigE4, sqrtMaturity, int256(_iv));
        if (_isPut) {
            return -AdvancedMath.calStandardNormalCDF(-d1E4);
        } else {
            return AdvancedMath.calStandardNormalCDF(d1E4);
        }
    }

    function getSqrtMaturity(uint256 _maturity) public pure returns (int256) {
        require(
            _maturity > 0 && _maturity < 31536000,
            "PriceCalculator: maturity must not have expired and less than 1 year"
        );

        return (AdvancedMath.sqrt(int256(_maturity)) * 1e16) / SQRT_YEAR_E8;
    }

    function calculatePriceOfRanges(
        Parameters memory _params,
        int256 _x0,
        int256 _x1,
        bool _isPut,
        uint256 _minDelta
    ) internal pure returns (int256 premium) {
        int256 lower = _x0 / RANGE;
        int256 upper = _x1 / RANGE;
        int256 cache;
        for (int256 i = lower; i <= upper; i++) {
            int256 x0 = 0;
            int256 x1 = RANGE;
            if (i == lower) {
                x0 = _x0 - i * RANGE;
            }
            if (i == upper) {
                x1 = _x1 - i * RANGE;
            }
            int256 p;
            (p, cache) = calculatePriceOfRange(_params, i, x0, x1, _isPut, cache, _minDelta);
            premium += p;
        }
        premium /= upper - lower + 1;
    }

    function calculatePriceOfRange(
        Parameters memory _params,
        int256 _tick,
        int256 _x0,
        int256 _x1,
        bool _isPut,
        int256 _start,
        uint256 _minDelta
    ) internal pure returns (int256, int256) {
        if (_start == 0) {
            _start = calOptionPrice(_params, _tick * RANGE, _isPut, _minDelta);
        }
        int256 end = calOptionPrice(_params, (_tick + 1) * RANGE, _isPut, _minDelta);
        // y = (end - start)/RANGE * x + start + instrict
        return ((_start + ((end - _start) * (_x1 + _x0)) / (2 * RANGE)), end);
    }

    function _calD1D2(
        int256 _logSigE4,
        int256 _sqrtMaturity,
        int256 _volatilityE8
    ) internal pure returns (int256 d1E4, int256 d2E4) {
        int256 sigE8 = (_volatilityE8 * _sqrtMaturity) / (1e8);
        d1E4 = ((_logSigE4 * 10**8) / sigE8) + (sigE8 / (2 * 10**4));
        d2E4 = d1E4 - (sigE8 / 10**4);
    }

    function calOptionPrice(
        Parameters memory _params,
        int256 _volatility,
        bool _isPut,
        uint256 _minDelta
    ) internal pure returns (int256 price) {
        int256 nd1E8;

        if (_volatility > 0) {
            (int256 d1E4, int256 d2E4) = _calD1D2(_params.logSigE4, _params.sqrtMaturity, _volatility);
            nd1E8 = AdvancedMath.calStandardNormalCDF(d1E4);
            int256 nd2E8 = AdvancedMath.calStandardNormalCDF(d2E4);
            price = (_params.spot * nd1E8 - _params.strike * nd2E8) / 1e8;
        }
        int256 lowestPrice;
        if (_isPut) {
            price = price - _params.spot + _params.strike;

            lowestPrice = (_params.strike > _params.spot) ? _params.strike - _params.spot : int256(0);
        } else {
            lowestPrice = (_params.spot > _params.strike) ? _params.spot - _params.strike : int256(0);
        }

        // delta cut-off
        // if option type is put, delta is `1 - N(d1)`
        // if option type is call, delta is `N(d1)`
        require((!_isPut && abs(nd1E8) >= _minDelta) || (_isPut && 1e8 - abs(nd1E8) >= _minDelta), "delta is too low");

        if (price < lowestPrice) {
            return lowestPrice;
        }

        return price;
    }

    function validateParameters(uint256 _spot, uint256 _strike) internal pure {
        require(_spot > 0 && _spot < 1e13, "PriceCalculator: spot price must be between 0 and 10^13");
        require(_strike > 0 && _strike < 1e13, "PriceCalculator: strike price must be between 0 and 10^13");
    }

    function abs(int256 x) internal pure returns (uint256) {
        return uint256(x >= 0 ? x : -x);
    }
}

// File: contracts/interfaces/IAave.sol



pragma solidity ^0.8.0;

interface LendingPool {
    function deposit(
        address asset,
        uint256 amount,
        address onBehalfOf,
        uint16 referralCode
    ) external;

    function withdraw(
        address asset,
        uint256 amount,
        address to
    ) external returns (uint256);

    function borrow(
        address asset,
        uint256 amount,
        uint256 interestRateMode,
        uint16 referralCode,
        address onBehalfOf
    ) external;

    function repay(
        address asset,
        uint256 amount,
        uint256 rateMode,
        address onBehalfOf
    ) external returns (uint256);
}

// File: contracts/interfaces/IWETH.sol



pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IWETH is IERC20 {
    function deposit() external payable;

    function withdraw(uint256 value) external;
}

// File: contracts/lib/AdvancedMath.sol



/// from https://github.com/LienFinance/bondmaker
pragma solidity ^0.8.0;

library AdvancedMath {
    /// @dev sqrt(2*PI) * 10^8
    int256 internal constant SQRT_2PI_E8 = 250662827;
    /// @dev PI * 10^8
    int256 internal constant PI_E8 = 314159265;
    /// @dev Napier's constant
    int256 internal constant E_E8 = 271828182;
    /// @dev Inverse of Napier's constant (1/e)
    int256 internal constant INV_E_E8 = 36787944;

    // for CDF
    int256 internal constant p = 23164190;
    int256 internal constant b1 = 31938153;
    int256 internal constant b2 = -35656378;
    int256 internal constant b3 = 178147793;
    int256 internal constant b4 = -182125597;
    int256 internal constant b5 = 133027442;

    /**
     * @dev Calculate an approximate value of the square root of x by Babylonian method.
     */
    function sqrt(int256 x) internal pure returns (int256 y) {
        require(x >= 0, "cannot calculate the square root of a negative number");
        int256 z = (x + 1) / 2;
        y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
    }

    /**
     * @dev Returns log(x) for any positive x.
     */
    function logTaylor(int256 inputE4) internal pure returns (int256 outputE4) {
        require(inputE4 > 1, "input should be positive number");
        int256 inputE8 = inputE4 * 1e4;
        // input x for _logTaylor1 is adjusted to 1/e < x < 1.
        while (inputE8 < INV_E_E8) {
            inputE8 = (inputE8 * E_E8) / 1e8;
            outputE4 -= 1e4;
        }
        while (inputE8 > 1e8) {
            inputE8 = (inputE8 * INV_E_E8) / 1e8;
            outputE4 += 1e4;
        }
        outputE4 += logTaylor1(inputE8 / 1e4 - 1e4);
    }

    /**
     * @notice Calculate an approximate value of the logarithm of input value by
     * Taylor expansion around 1.
     * @dev log(x + 1) = x - 1/2 x^2 + 1/3 x^3 - 1/4 x^4 + 1/5 x^5
     *                     - 1/6 x^6 + 1/7 x^7 - 1/8 x^8 + ...
     */
    function logTaylor1(int256 inputE4) internal pure returns (int256 outputE4) {
        outputE4 =
            inputE4 -
            inputE4**2 /
            (2 * 1e4) +
            inputE4**3 /
            (3 * 1e8) -
            inputE4**4 /
            (4 * 1e12) +
            inputE4**5 /
            (5 * 1e16) -
            inputE4**6 /
            (6 * 1e20) +
            inputE4**7 /
            (7 * 1e24) -
            inputE4**8 /
            (8 * 1e28);
    }

    /**
     * @notice Calculate the cumulative distribution function of standard normal
     * distribution.
     * @dev Abramowitz and Stegun, Handbook of Mathematical Functions (1964)
     * http://people.math.sfu.ca/~cbm/aands/
     * errors are less than 0.7% at -3.2
     */
    function calStandardNormalCDF(int256 inputE4) internal pure returns (int256 outputE8) {
        require(inputE4 < 440 * 1e4 && inputE4 > -440 * 1e4, "input is too large");
        int256 _inputE4 = inputE4 > 0 ? inputE4 : inputE4 * (-1);
        int256 t = 1e16 / (1e8 + (p * _inputE4) / 1e4);
        int256 X2 = (inputE4 * inputE4) / 2;
        int256 X3 = (X2 * X2) / 1e8;
        int256 X4 = (X3 * X2) / 1e8;
        int256 exp2X2 = 1e8 +
            X2 +
            (X3 / 2) +
            (X4 / 6) +
            ((X3 * X3) / (24 * 1e8)) +
            ((X2 * (X3 * X3)) / (120 * 1e16)) +
            ((X4 * X4) / (720 * 1e8)) +
            ((X2 * (X4 * X4)) / (5040 * 1e16)) +
            ((X3 * (X4 * X4)) / (40320 * 1e16)) +
            ((X4 * X4 * X4) / (362880 * 1e16)) +
            ((X2 * (X4 * X4 * X4)) / (3628800 * 1e24)) +
            ((X3 * (X4 * X4 * X4)) / (39916800 * 1e24));

        int256 Z = (1e24 / exp2X2) / SQRT_2PI_E8;
        int256 y = (b5 * t) / 1e8;
        y = ((y + b4) * t) / 1e8;
        y = ((y + b3) * t) / 1e8;
        y = ((y + b2) * t) / 1e8;
        y = 1e8 - (Z * ((y + b1) * t)) / 1e16;
        return inputE4 > 0 ? y : 1e8 - y;
    }
}

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



// SPDX-License-Identifier: MIT
pragma solidity >=0.6.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
    );

}