contract UniV3PairManager is IUniV3PairManager, Governable {
  /// @inheritdoc IERC20Metadata
  string public override name;

  /// @inheritdoc IERC20Metadata
  string public override symbol;

  /// @inheritdoc IERC20
  uint256 public override totalSupply = 0;

  /// @inheritdoc IPairManager
  address public immutable override token0;

  /// @inheritdoc IPairManager
  address public immutable override token1;

  /// @inheritdoc IPairManager
  address public immutable override pool;

  /// @inheritdoc IUniV3PairManager
  uint24 public immutable override fee;

  /// @inheritdoc IUniV3PairManager
  uint160 public immutable override sqrtRatioAX96;

  /// @inheritdoc IUniV3PairManager
  uint160 public immutable override sqrtRatioBX96;

  /// @notice Lowest possible tick in the Uniswap's curve
  int24 private constant _TICK_LOWER = -887200;

  /// @notice Highest possible tick in the Uniswap's curve
  int24 private constant _TICK_UPPER = 887200;

  /// @inheritdoc IERC20Metadata
  //solhint-disable-next-line const-name-snakecase
  uint8 public constant override decimals = 18;

  /// @inheritdoc IERC20
  mapping(address => mapping(address => uint256)) public override allowance;

  /// @inheritdoc IERC20
  mapping(address => uint256) public override balanceOf;

  /// @notice Struct that contains token0, token1, and fee of the Uniswap pool
  PoolAddress.PoolKey private _poolKey;

  constructor(address _pool, address _governance) Governable(_governance) {
    pool = _pool;
    uint24 _fee = IUniswapV3Pool(_pool).fee();
    fee = _fee;
    address _token0 = IUniswapV3Pool(_pool).token0();
    address _token1 = IUniswapV3Pool(_pool).token1();
    token0 = _token0;
    token1 = _token1;
    name = string(abi.encodePacked('Keep3rLP - ', ERC20(_token0).symbol(), '/', ERC20(_token1).symbol()));
    symbol = string(abi.encodePacked('kLP-', ERC20(_token0).symbol(), '/', ERC20(_token1).symbol()));
    sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(_TICK_LOWER);
    sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(_TICK_UPPER);
    _poolKey = PoolAddress.PoolKey({token0: _token0, token1: _token1, fee: _fee});
  }

  // This low-level function should be called from a contract which performs important safety checks
  /// @inheritdoc IUniV3PairManager
  function mint(
    uint256 amount0Desired,
    uint256 amount1Desired,
    uint256 amount0Min,
    uint256 amount1Min,
    address to
  ) external override returns (uint128 liquidity) {
    (liquidity, , ) = _addLiquidity(amount0Desired, amount1Desired, amount0Min, amount1Min);
    _mint(to, liquidity);
  }

  /// @inheritdoc IUniV3PairManager
  function uniswapV3MintCallback(
    uint256 amount0Owed,
    uint256 amount1Owed,
    bytes calldata data
  ) external override {
    MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
    if (msg.sender != pool) revert OnlyPool();
    if (amount0Owed > 0) _pay(decoded._poolKey.token0, decoded.payer, pool, amount0Owed);
    if (amount1Owed > 0) _pay(decoded._poolKey.token1, decoded.payer, pool, amount1Owed);
  }

  /// @inheritdoc IUniV3PairManager
  function burn(
    uint128 liquidity,
    uint256 amount0Min,
    uint256 amount1Min,
    address to
  ) external override returns (uint256 amount0, uint256 amount1) {
    (amount0, amount1) = IUniswapV3Pool(pool).burn(_TICK_LOWER, _TICK_UPPER, liquidity);

    if (amount0 < amount0Min || amount1 < amount1Min) revert ExcessiveSlippage();

    IUniswapV3Pool(pool).collect(to, _TICK_LOWER, _TICK_UPPER, uint128(amount0), uint128(amount1));
    _burn(msg.sender, liquidity);
  }

  /// @inheritdoc IUniV3PairManager
  function collect() external override onlyGovernance returns (uint256 amount0, uint256 amount1) {
    (, , , uint128 tokensOwed0, uint128 tokensOwed1) = IUniswapV3Pool(pool).positions(
      keccak256(abi.encodePacked(address(this), _TICK_LOWER, _TICK_UPPER))
    );
    (amount0, amount1) = IUniswapV3Pool(pool).collect(governance, _TICK_LOWER, _TICK_UPPER, tokensOwed0, tokensOwed1);
  }

  /// @inheritdoc IUniV3PairManager
  function position()
    external
    view
    override
    returns (
      uint128 liquidity,
      uint256 feeGrowthInside0LastX128,
      uint256 feeGrowthInside1LastX128,
      uint128 tokensOwed0,
      uint128 tokensOwed1
    )
  {
    (liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128, tokensOwed0, tokensOwed1) = IUniswapV3Pool(pool).positions(
      keccak256(abi.encodePacked(address(this), _TICK_LOWER, _TICK_UPPER))
    );
  }

  /// @inheritdoc IERC20
  function approve(address spender, uint256 amount) external override returns (bool) {
    allowance[msg.sender][spender] = amount;

    emit Approval(msg.sender, spender, amount);
    return true;
  }

  /// @inheritdoc IERC20
  function transfer(address to, uint256 amount) external override returns (bool) {
    _transferTokens(msg.sender, to, amount);
    return true;
  }

  /// @inheritdoc IERC20
  function transferFrom(
    address from,
    address to,
    uint256 amount
  ) external override returns (bool) {
    address spender = msg.sender;
    uint256 spenderAllowance = allowance[from][spender];

    if (spender != from && spenderAllowance != type(uint256).max) {
      uint256 newAllowance = spenderAllowance - amount;
      allowance[from][spender] = newAllowance;

      emit Approval(from, spender, newAllowance);
    }

    _transferTokens(from, to, amount);
    return true;
  }

  /// @notice Adds liquidity to an initialized pool
  /// @dev Reverts if the returned amount0 is less than amount0Min or if amount1 is less than amount1Min
  /// @dev This function calls the mint function of the corresponding Uniswap pool, which in turn calls UniswapV3Callback
  /// @param amount0Desired The amount of token0 we would like to provide
  /// @param amount1Desired The amount of token1 we would like to provide
  /// @param amount0Min The minimum amount of token0 we want to provide
  /// @param amount1Min The minimum amount of token1 we want to provide
  /// @return liquidity The calculated liquidity we get for the token amounts we provided
  /// @return amount0 The amount of token0 we ended up providing
  /// @return amount1 The amount of token1 we ended up providing
  function _addLiquidity(
    uint256 amount0Desired,
    uint256 amount1Desired,
    uint256 amount0Min,
    uint256 amount1Min
  )
    internal
    returns (
      uint128 liquidity,
      uint256 amount0,
      uint256 amount1
    )
  {
    (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(pool).slot0();

    liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, amount0Desired, amount1Desired);

    (amount0, amount1) = IUniswapV3Pool(pool).mint(
      address(this),
      _TICK_LOWER,
      _TICK_UPPER,
      liquidity,
      abi.encode(MintCallbackData({_poolKey: _poolKey, payer: msg.sender}))
    );

    if (amount0 < amount0Min || amount1 < amount1Min) revert ExcessiveSlippage();
  }

  /// @notice Transfers the passed-in token from the payer to the recipient for the corresponding value
  /// @param token The token to be transferred to the recipient
  /// @param from The address of the payer
  /// @param to The address of the passed-in tokens recipient
  /// @param value How much of that token to be transferred from payer to the recipient
  function _pay(
    address token,
    address from,
    address to,
    uint256 value
  ) internal {
    _safeTransferFrom(token, from, to, value);
  }

  /// @notice Mints Keep3r credits to the passed-in address of recipient and increases total supply of Keep3r credits by the corresponding amount
  /// @param to The recipient of the Keep3r credits
  /// @param amount The amount Keep3r credits to be minted to the recipient
  function _mint(address to, uint256 amount) internal {
    totalSupply += amount;
    balanceOf[to] += amount;
    emit Transfer(address(0), to, amount);
  }

  /// @notice Burns Keep3r credits to the passed-in address of recipient and reduces total supply of Keep3r credits by the corresponding amount
  /// @param to The address that will get its Keep3r credits burned
  /// @param amount The amount Keep3r credits to be burned from the recipient/recipient
  function _burn(address to, uint256 amount) internal {
    totalSupply -= amount;
    balanceOf[to] -= amount;
    emit Transfer(to, address(0), amount);
  }

  /// @notice Transfers amount of Keep3r credits between two addresses
  /// @param from The user that transfers the Keep3r credits
  /// @param to The user that receives the Keep3r credits
  /// @param amount The amount of Keep3r credits to be transferred
  function _transferTokens(
    address from,
    address to,
    uint256 amount
  ) internal {
    balanceOf[from] -= amount;
    balanceOf[to] += amount;

    emit Transfer(from, to, amount);
  }

  /// @notice Transfers the passed-in token from the specified "from" to the specified "to" for the corresponding value
  /// @dev Reverts with IUniV3PairManager#UnsuccessfulTransfer if the transfer was not successful,
  ///      or if the passed data length is different than 0 and the decoded data is not a boolean
  /// @param token The token to be transferred to the specified "to"
  /// @param from  The address which is going to transfer the tokens
  /// @param value How much of that token to be transferred from "from" to "to"
  function _safeTransferFrom(
    address token,
    address from,
    address to,
    uint256 value
  ) internal {
    // solhint-disable-next-line avoid-low-level-calls
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
    if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert UnsuccessfulTransfer();
  }
}
function _burn(address to, uint256 amount) internal {
  totalSupply -= amount;
  balanceOf[to] -= amount;
  emit Transfer(to, address(0), amount);
}

/// @notice Burns Keep3r credits to the passed-in address of recipient and reduces total supply of Keep3r credits by the corresponding amount
/// @param to The address that will get its Keep3r credits burned
/// @param amount The amount Keep3r credits to be burned from the recipient/recipient