// Contract: TimeLoanPairFactory
// Target function: function _withdrawLiquidity(address asset, uint amount) internal returns (uint withdrew)
Setter(address) external;
}

interface IUniswapV2Pair {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
    function decimals() external pure returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);

    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function PERMIT_TYPEHASH() external pure returns (bytes32);
    function nonces(address owner) external view returns (uint);

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);

    function MINIMUM_LIQUIDITY() external pure returns (uint);
    function factory() external view returns (address);
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function price0CumulativeLast() external view returns (uint);
    function price1CumulativeLast() external view returns (uint);
    function kLast() external view returns (uint);

    function mint(address to) external returns (uint liquidity);
    function burn(address to) external returns (uint amount0, uint amount1);
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
    function skim(address to) external;
    function sync() external;

    function initialize(address, address) external;
}

interface IUniswapV2Router01 {
    function factory() external pure returns (address);
    function WETH() external pure returns (address);

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB, uint liquidity);
    function addLiquidityETH(
        address token,
        uint amountTokenDesired,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external payable returns (uint amountToken, uint amountETH, uint liquidity);
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB);
    function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external returns (uint amountToken, uint amountETH);
    function removeLiquidityWithPermit(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountA, uint amountB);
    function removeLiquidityETHWithPermit(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountToken, uint amountETH);
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
    function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        payable
        returns (uint[] memory amounts);
    function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
        external
        returns (uint[] memory amounts);
    function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        returns (uint[] memory amounts);
    function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
        external
        payable
        returns (uint[] memory amounts);

    function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
    function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
    function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts);
}

interface IUniswapV2Router02 is IUniswapV2Router01 {
    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external returns (uint amountETH);
    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountETH);

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external;
    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external payable;
    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external;
}

interface IUniswapOracleRouter {
    function quote(address tokenIn, address tokenOut, uint amountIn) external view returns (uint amountOut);
}

contract TimeLoanPair {
    using SafeMath for uint;
    
    /// @notice EIP-20 token name for this token
    string public constant name = "Time Loan Pair LP";

    /// @notice EIP-20 token symbol for this token
    string public symbol;

    /// @notice EIP-20 token decimals for this token
    uint8 public constant decimals = 18;
    
    /// @notice Total number of tokens in circulation
    uint public totalSupply = 0; // Initial 0
    
    mapping (address => mapping (address => uint)) internal allowances;
    mapping (address => uint) internal balances;

    /// @notice The EIP-712 typehash for the contract's domain
    bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint chainId,address verifyingContract)");

    /// @notice The EIP-712 typehash for the permit struct used by the contract
    bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint value,uint nonce,uint deadline)");

    /// @notice A record of states for signing / validating signatures
    mapping (address => uint) public nonces;

    /// @notice The standard EIP-20 transfer event
    event Transfer(address indexed from, address indexed to, uint amount);

    /// @notice The standard EIP-20 approval event
    event Approval(address indexed owner, address indexed spender, uint amount);

    /// @notice Uniswap V2 Router used for all swaps and liquidity management
    IUniswapV2Router02 public constant UNI = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
    
    /// @notice Uniswap Oracle Router used for all 24 hour TWAP price metrics
    IUniswapOracleRouter public constant ORACLE = IUniswapOracleRouter(0x0b5A6b318c39b60e7D8462F888e7fbA89f75D02F);
    
    /// @notice The underlying Uniswap Pair used for loan liquidity
    address public pair;
    
    /// @notice The token0 of the Uniswap Pair
    address public token0;
    
    /// @notice The token1 of the Uniswap Pair
    address public token1;
    
   
    /// @notice Deposited event for creditor/LP
    event Deposited(address indexed creditor, address indexed collateral, uint shares, uint credit);
    /// @notice Withdawn event for creditor/LP
    event Withdrew(address indexed creditor, address indexed collateral, uint shares, uint credit);
    
    /// @notice The borrow event for any borrower
    event Borrowed(uint id, address indexed borrower, address indexed collateral, address indexed borrowed, uint creditIn, uint amountOut, uint created, uint expire);
    /// @notice The close loan event when processing expired loans
    event Repaid(uint id, address indexed borrower, address indexed collateral, address indexed borrowed, uint creditIn, uint amountOut, uint created, uint expire);
    /// @notice The close loan event when processing expired loans
    event Closed(uint id, address indexed borrower, address indexed collateral, address indexed borrowed, uint creditIn, uint amountOut, uint created, uint expire);
    
    /// @notice 0.6% initiation fee for all loans
    uint public constant FEE = 600; // 0.6% loan initiation fee
    
    /// @notice 105% liquidity buffer on withdrawing liquidity
    uint public constant BUFFER = 105000; // 105% liquidity buffer
    
    /// @notice 80% loan to value ratio
    uint public constant LTV = 80000; // 80% loan to value ratio
    
    /// @notice base for all % based calculations 
    uint public constant BASE = 100000;
    
    /// @notice the delay for a position to be closed
    uint public constant DELAY = 6600; // ~24 hours till position is closed
    
    
    struct position {
        address owner;
        address collateral;
        address borrowed;
        uint creditIn;
        uint amountOut;
        uint liquidityInUse;
        uint created;
        uint expire;
        bool open;
    }
    
    /// @notice array of all loan positions
    position[] public positions;
    
    /// @notice the tip index of the positions array
    uint public nextIndex;
    
    /// @notice the last index processed by the contract
    uint public processedIndex;
    
    /// @notice mapping of loans assigned to users
    mapping(address => uint[]) public loans;
    
    /// @notice constructor takes a uniswap pair as an argument to set its 2 borrowable assets
    constructor(IUniswapV2Pair _pair) public {
        symbol = string(abi.encodePacked(IUniswapV2Pair(_pair.token0()).symbol(), "-", IUniswapV2Pair(_pair.token1()).symbol()));
        pair = address(_pair);
        token0 = _pair.token0();
        token1 = _pair.token1();
    }
    
    /// @notice total liquidity deposited
    uint public liquidityDeposits;
    /// @notice total liquidity withdrawn
    uint public liquidityWithdrawals;
    /// @notice total liquidity added via addLiquidity
    uint public liquidityAdded;
    /// @notice total liquidity removed via removeLiquidity
    uint public liquidityRemoved;
    /// @notice total liquidity currently in use by pending loans
    uint public liquidityInUse;
    /// @notice total liquidity freed up from closed loans
    uint public liquidityFreed;
    
    /**
     * @notice the current net liquidity positions
     * @return the net liquidity sum
     */
    function liquidityBalance() public view returns (uint) {
        return liquidityDeposits
                .sub(liquidityWithdrawals)
                .add(liquidityAdded)
                .sub(liquidityRemoved)
                .add(liquidityFreed)
                .sub(liquidityInUse);
    }
    
    function _mint(address dst, uint amount) internal {
        // mint the amount
        totalSupply = totalSupply.add(amount);

        // transfer the amount to the recipient
        balances[dst] = balances[dst].add(amount);
        emit Transfer(address(0), dst, amount);
    }
    
    function _burn(address dst, uint amount) internal {
        // burn the amount
        totalSupply = totalSupply.sub(amount, "TimeLoans::_burn: underflow");

        // transfer the amount to the recipient
        balances[dst] = balances[dst].sub(amount, "TimeLoans::_burn: underflow");
        emit Transfer(dst, address(0), amount);
    }
    
    /**
     * @notice withdraw all liquidity from msg.sender shares
     * @return success/failure
     */
    function withdrawAll() external returns (bool) {
        return withdraw(balances[msg.sender]);
    }
    
    /**
     * @notice withdraw `_shares` amount of liquidity for user
     * @param _shares the amount of shares to burn for liquidity
     * @return success/failure
     */
    function withdraw(uint _shares) public returns (bool) {
        uint r = liquidityBalance().mul(_shares).div(totalSupply);
        _burn(msg.sender, _shares);
        
        require(IERC20(pair).balanceOf(address(this)) > r, "TimeLoans::withdraw: insufficient liquidity to withdraw, try depositLiquidity()");
        
        IERC20(pair).transfer(msg.sender, r);
        liquidityWithdrawals = liquidityWithdrawals.add(r);
        emit Withdrew(msg.sender, pair, _shares, r);
        return true;
    }
    
    /**
     * @notice deposit all liquidity from msg.sender
     * @return success/failure
     */
    function depositAll() external returns (bool) {
        return deposit(IERC20(pair).balanceOf(msg.sender));
    }
    
    /**
     * @notice deposit `amount` amount of liquidity for user
     * @param amount the amount of liquidity to add for shares
     * @return success/failure
     */
    function deposit(uint amount) public returns (bool) {
        IERC20(pair).transferFrom(msg.sender, address(this), amount);
        uint _shares = 0;
        if (liquidityBalance() == 0) {
            _shares = amount;
        } else {
            _shares = amount.mul(totalSupply).div(liquidityBalance());
        }
        _mint(msg.sender, _shares);
        liquidityDeposits = liquidityDeposits.add(amount);
        emit Deposited(msg.sender, pair, _shares, amount);
        return true;
    }
    
    /**
     * @notice batch close any pending open loans that have expired
     * @param size the maximum size of batch to execute
     * @return the last index processed
     */
    function closeInBatches(uint size) external returns (uint) {
        uint i = processedIndex;
        for (; i < size; i++) {
            close(i);
        }
        processedIndex = i;
        return processedIndex;
    }
    
    /**
     * @notice iterate through all open loans and close
     * @return the last index processed
     */
    function closeAllOpen() external returns (uint) {
        uint i = processedIndex;
        for (; i < nextIndex; i++) {
            close(i);
        }
        processedIndex = i;
        return processedIndex;
    }
    
    /**
     * @notice close a specific loan based on id
     * @param id the `id` of the given loan to close
     * @return success/failure
     */
    function close(uint id) public returns (bool) {
        position storage _pos = positions[id];
        if (_pos.owner == address(0x0)) {
            return false;
        }
        if (!_pos.open) {
            return false;
        }
        if (_pos.expire > block.number) {
            return false;
        }
        _pos.open = false;
        liquidityInUse = liquidityInUse.sub(_pos.liquidityInUse, "TimeLoans::close: liquidityInUse overflow");
        liquidityFreed = liquidityFreed.add(_pos.liquidityInUse);
        emit Closed(id, _pos.owner, _pos.collateral, _pos.borrowed, _pos.creditIn, _pos.amountOut, _pos.created, _pos.expire);
        return true;
    }
        
    /**
     * @notice returns the available liquidity (including LP tokens) for a given asset
     * @param asset the asset to calculate liquidity for
     * @return the amount of liquidity available
     */
    function liquidityOf(address asset) public view returns (uint) {
        return IERC20(asset).balanceOf(address(this)).
                add(IERC20(asset).balanceOf(pair)
                    .mul(IERC20(pair).balanceOf(address(this)))
                    .div(IERC20(pair).totalSupply()));
    }
    
    /**
     * @notice calculates the amount of liquidity to burn to get the amount of asset
     * @param amount the amount of asset required as output 
     * @return the amount of liquidity to burn
     */
    function calculateLiquidityToBurn(address asset, uint amount) public view returns (uint) {
        return IERC20(pair).totalSupply()
                .mul(amount)
                .div(IERC20(asset).balanceOf(pair));
    }