// File: contracts/DexUtils.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';

interface IERC20Decimals is IERC20 {
  function decimals() external view returns (uint8);
}

/**
 * DEX Utilities
 */
contract DexUtils {
  using SafeMath for uint256;

  // PancakeSwap: 0x10ED43C718714eb63d5aA57B78B54704E256024E
  // Uniswap V2: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
  IUniswapV2Router02 uniswapV2Router;
  IUniswapV2Factory uniswapV2Factory;

  address public wrappedNative;
  address public stableToken;

  constructor(
    address _dexRouter,
    address _wrappedNative,
    address _stableToken
  ) {
    uniswapV2Router = IUniswapV2Router02(_dexRouter);
    uniswapV2Factory = IUniswapV2Factory(uniswapV2Router.factory());
    wrappedNative = _wrappedNative;
    stableToken = _stableToken;
  }

  // returns main stable pair price adjusted to 18 decimals
  // NOTE: assumes the primary DEX pair for main token is wrapped native
  function getMainPriceViaNativePair(address token)
    external
    view
    returns (uint256)
  {
    (
      uint256 mnPriceAdjusted,
      address mnToken0,
      address mnToken1
    ) = _getTokenPrice(token, wrappedNative);
    (
      uint256 nsPriceAdjusted,
      address nsToken0,
      address nsToken1
    ) = _getTokenPrice(wrappedNative, stableToken);

    // *** WHAT WE WANT***: stable / main
    // *** REMEMBER ***: _getTokenPrice returns the priceAdjusted as reserves1 / reserves0
    if (mnToken0 == nsToken0) {
      // Scenario A: 1. main / native -- 2. stable / native
      // (stable / native) / (main / native) = stable / main
      return nsPriceAdjusted.mul(10**18).div(mnPriceAdjusted);
    } else if (mnToken1 == nsToken1) {
      // Scenario B: 1. native / main -- 2. native / stable
      // (native / main) / (native / stable) = stable / main
      return mnPriceAdjusted.mul(10**18).div(nsPriceAdjusted);
    } else if (mnToken1 == nsToken0) {
      // Scenario C: 1. native / main -- 2. stable / native
      // (native / main) * (stable / native) = stable / main
      // NOTE each price is adjusted by 10^18, so need to offset by 10^18
      return nsPriceAdjusted.mul(mnPriceAdjusted).div(uint256(10**18));
    }

    // Scenario D: 1. main / native -- 2. native / stable
    // 1 / ((native / stable) * (main / native)) = stable / main
    // NOTE: each price is adjusted by 10^18, so need to offset by 10^36, then add 18 decimals (hence 10^54)
    return uint256(10**54).div(nsPriceAdjusted.mul(mnPriceAdjusted));
  }

  // returns uniswap pair price moved to 18 decimals
  function _getTokenPrice(address _t0, address _t1)
    private
    view
    returns (
      uint256,
      address,
      address
    )
  {
    IUniswapV2Pair dexPair = IUniswapV2Pair(uniswapV2Factory.getPair(_t0, _t1));
    (uint112 res0, uint112 res1, ) = dexPair.getReserves();
    address t0 = dexPair.token0();
    uint8 t0Dec = IERC20Decimals(t0).decimals();
    address t1 = dexPair.token1();
    uint8 t1Dec = IERC20Decimals(t1).decimals();

    return (
      uint256(res1).mul(10**18).mul(10**t0Dec).div(uint256(res0)).div(
        10**t1Dec
      ),
      t0,
      t1
    );
  }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol)

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/utils/math/SafeMath.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/math/SafeMath.sol)

pragma solidity ^0.8.0;

// CAUTION
// This version of SafeMath should only be used with Solidity 0.8 or later,
// because it relies on the compiler's built in overflow checks.

/**
 * @dev Wrappers over Solidity's arithmetic operations.
 *
 * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler
 * now has built in overflow checking.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the substraction of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b > a) return (false, 0);
            return (true, a - b);
        }
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
            if (a == 0) return (true, 0);
            uint256 c = a * b;
            if (c / a != b) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the division of two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a / b);
        }
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a % b);
        }
    }

    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return a - b;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        return a * b;
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator.
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return a / b;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return a % b;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {trySub}.
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b <= a, errorMessage);
            return a - b;
        }
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a / b;
        }
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting with custom message when dividing by zero.
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {tryMod}.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a % b;
        }
    }
}

// File: @uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol



pragma solidity >=0.5.0;

interface IUniswapV2Factory {
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    function feeTo() external view returns (address);
    function feeToSetter() external view returns (address);

    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function allPairs(uint) external view returns (address pair);
    function allPairsLength() external view returns (uint);

    function createPair(address tokenA, address tokenB) external returns (address pair);

    function setFeeTo(address) external;
    function setFeeToSetter(address) external;
}

// File: @uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol



pragma solidity >=0.5.0;

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;
}

// File: @uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol



pragma solidity >=0.6.2;

import './IUniswapV2Router01.sol';

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;
}

// File: @uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol



pragma solidity >=0.6.2;

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);
}

// File: contracts/unaudited/BuybackAndBurnOKLG.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';
import '../OKLGWithdrawable.sol';

contract BuybackAndBurnOKLG is OKLGWithdrawable {
  address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
  address public receiver = DEAD;
  address public oklg = 0x5dBB9F64cd96E2DbBcA58d14863d615B67B42f2e;
  IUniswapV2Router02 private router;

  constructor() {
    // PancakeSwap: 0x10ED43C718714eb63d5aA57B78B54704E256024E
    // Uniswap V2: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
    router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
  }

  function buyAndBurn() external payable {
    require(msg.value > 0, 'Must send ETH to buy and burn OKLG');

    address[] memory path = new address[](2);
    path[0] = router.WETH();
    path[1] = oklg;

    router.swapExactETHForTokensSupportingFeeOnTransferTokens{
      value: msg.value
    }(
      0, // accept any amount of tokens
      path,
      receiver,
      block.timestamp
    );
  }

  function setOklg(address _oklg) external onlyOwner {
    oklg = _oklg;
  }

  function setReceiver(address _receiver) external onlyOwner {
    receiver = _receiver;
  }
}

// File: contracts/OKLGWithdrawable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';

/**
 * @title OKLGWithdrawable
 * @dev Supports being able to get tokens or ETH out of a contract with ease
 */
contract OKLGWithdrawable is Ownable {
  function withdrawTokens(address _tokenAddy, uint256 _amount)
    external
    onlyOwner
  {
    IERC20 _token = IERC20(_tokenAddy);
    _amount = _amount > 0 ? _amount : _token.balanceOf(address(this));
    require(_amount > 0, 'make sure there is a balance available to withdraw');
    _token.transfer(owner(), _amount);
  }

  function withdrawETH() external onlyOwner {
    payable(owner()).call{ value: address(this).balance }('');
  }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)

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() {
        _transferOwnership(_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 {
        _transferOwnership(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");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/interfaces/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (interfaces/IERC20.sol)

pragma solidity ^0.8.0;

import "../token/ERC20/IERC20.sol";

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

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) {
        return msg.data;
    }
}

// File: contracts/unaudited/OKLApeRewardsBooster.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import '../interfaces/IConditional.sol';
import '../interfaces/IMultiplier.sol';
import '../OKLGWithdrawable.sol';

interface IERC20Decimals is IERC20 {
  function decimals() external view returns (uint8);
}

/**
 * @title OKLApeRewardsBooster
 * @dev Calculates the boost amount for OKLG native rewards.
 */
contract OKLApeRewardsBooster is IConditional, IMultiplier, OKLGWithdrawable {
  using SafeMath for uint256;

  struct Booster {
    uint256 baseBoost; // percentage boost: 100, 50, 25, etc.
    uint256 maxOKLGBalance; // max OKLG balance in this range of boosters
    uint256 maxNFTBalance; // maximum NFT balance to continue boosting rewards
  }

  IERC20Decimals oklg;
  IERC721 oklApe;
  Booster[] multipliers;

  constructor(address _oklg, address _oklApe) {
    oklg = IERC20Decimals(_oklg);
    oklApe = IERC721(_oklApe);

    // seed initial rewards boosters
    multipliers.push(
      Booster({
        baseBoost: 50,
        maxOKLGBalance: uint256(15_000_000).mul(10**oklg.decimals()),
        maxNFTBalance: 6
      })
    );

    multipliers.push(
      Booster({
        baseBoost: 25,
        maxOKLGBalance: uint256(80_000_000).mul(10**oklg.decimals()),
        maxNFTBalance: 8
      })
    );

    multipliers.push(
      Booster({
        baseBoost: 15,
        maxOKLGBalance: uint256(420_690_000_000).mul(10**oklg.decimals()),
        maxNFTBalance: 10
      })
    );
  }

  // required by rewards booster logic in rewards contract to determine if eligible for booster at all
  function passesTest(address wallet) external view override returns (bool) {
    return wallet == address(0) ? false : oklApe.balanceOf(wallet) >= 1;
  }

  // returns number indicating percentage boost (0 == 0%, 1 == 1%, etc.)
  function getMultiplier(address wallet)
    external
    view
    override
    returns (uint256)
  {
    if (wallet == address(0)) return 0;
    uint256 _userOKLGBalance = oklg.balanceOf(wallet);
    if (_userOKLGBalance == 0) return 0;

    uint256 _userNFTBalance = oklApe.balanceOf(wallet);
    if (_userNFTBalance == 0) return 0;

    for (uint256 _i = 0; _i < multipliers.length; _i++) {
      Booster memory mult = multipliers[_i];
      if (_userOKLGBalance > mult.maxOKLGBalance) continue;

      uint256 nftBalanceAdjusted = _userNFTBalance > mult.maxNFTBalance
        ? mult.maxNFTBalance
        : _userNFTBalance;
      return mult.baseBoost.mul(nftBalanceAdjusted);
    }
    return 0;
  }

  function getOklgAddress() external view returns (address) {
    return address(oklg);
  }

  function getOklApeAddress() external view returns (address) {
    return address(oklApe);
  }

  function setOklg(address _oklgAddy) external onlyOwner {
    oklg = IERC20Decimals(_oklgAddy);
  }

  function setOklApe(address _oklApe) external onlyOwner {
    oklApe = IERC721(_oklApe);
  }

  function setAllMultipliers(Booster[] memory _boosters) external onlyOwner {
    delete multipliers;
    for (uint256 _i = 0; _i < _boosters.length; _i++) {
      multipliers.push(_boosters[_i]);
    }
  }
}

// File: @openzeppelin/contracts/interfaces/IERC721.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (interfaces/IERC721.sol)

pragma solidity ^0.8.0;

import "../token/ERC721/IERC721.sol";

// File: contracts/interfaces/IConditional.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

/**
 * @dev Interface for the Buy Back Reward contract that can be used to build
 * custom logic to elevate user rewards
 */
interface IConditional {
  /**
   * @dev Returns whether a wallet passes the test.
   */
  function passesTest(address wallet) external view returns (bool);
}

// File: contracts/interfaces/IMultiplier.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IMultiplier {
  function getMultiplier(address wallet) external view returns (uint256);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;

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

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

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: contracts/OKLGRewards.sol



// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import './interfaces/IConditional.sol';
import './interfaces/IMultiplier.sol';
import './OKLGWithdrawable.sol';

interface IOKLG is IERC20 {
  function getLastETHRewardsClaim(address wallet)
    external
    view
    returns (uint256);
}

contract OKLGRewards is OKLGWithdrawable {
  using SafeMath for uint256;

  address public constant deadAddress =
    0x000000000000000000000000000000000000dEaD;
  IOKLG private _oklg = IOKLG(0x5f67df361f568e185aA0304A57bdE4b8028d059E);

  uint256 public rewardsClaimTimeSeconds = 60 * 60 * 12; // 12 hours
  mapping(address => uint256) private _rewardsLastClaim;

  uint256 public boostRewardsPercent = 50;
  address public boostRewardsMultiplierContract;
  address public boostRewardsContract;

  event SendETHRewards(address to, uint256 amountETH);
  event SendTokenRewards(address to, address token, uint256 amount);

  function ethRewardsBalance() external view returns (uint256) {
    return address(this).balance;
  }

  function getLastETHRewardsClaim(address wallet)
    external
    view
    returns (uint256)
  {
    return
      _rewardsLastClaim[wallet] > _oklg.getLastETHRewardsClaim(wallet)
        ? _rewardsLastClaim[wallet]
        : _oklg.getLastETHRewardsClaim(wallet);
  }

  function setBoostMultiplierContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IMultiplier _contCheck = IMultiplier(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.getMultiplier(address(0)) >= 0,
        'contract does not implement interface'
      );
    }
    boostRewardsMultiplierContract = _contract;
  }

  function setBoostRewardsContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IConditional _contCheck = IConditional(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.passesTest(address(0)) == true ||
          _contCheck.passesTest(address(0)) == false,
        'contract does not implement interface'
      );
    }
    boostRewardsContract = _contract;
  }

  function setBoostRewardsPercent(uint256 _perc) external onlyOwner {
    boostRewardsPercent = _perc;
  }

  function setRewardsClaimTimeSeconds(uint256 _seconds) external onlyOwner {
    rewardsClaimTimeSeconds = _seconds;
  }

  function setOklgContract(address cont) external onlyOwner {
    _oklg = IOKLG(cont);
  }

  function getOklgContract() external view returns (address) {
    return address(_oklg);
  }

  function getBoostMultiplier(address wallet) public view returns (uint256) {
    return
      boostRewardsMultiplierContract == address(0)
        ? boostRewardsPercent
        : IMultiplier(boostRewardsMultiplierContract).getMultiplier(wallet);
  }

  function calculateETHRewards(address wallet) public view returns (uint256) {
    uint256 baseRewards = address(this)
      .balance
      .mul(_oklg.balanceOf(wallet))
      .div(
        _oklg.totalSupply().sub(_oklg.balanceOf(deadAddress)) // circulating supply
      );
    uint256 rewardsWithBooster = eligibleForRewardBooster(wallet)
      ? baseRewards.add(baseRewards.mul(getBoostMultiplier(wallet)).div(10**2))
      : baseRewards;
    return
      rewardsWithBooster > address(this).balance
        ? baseRewards
        : rewardsWithBooster;
  }

  function calculateTokenRewards(address wallet, address tokenAddress)
    public
    view
    returns (uint256)
  {
    IERC20 token = IERC20(tokenAddress);
    uint256 contractTokenBalance = token.balanceOf(address(this));
    uint256 baseRewards = contractTokenBalance.mul(_oklg.balanceOf(wallet)).div(
        _oklg.totalSupply().sub(_oklg.balanceOf(deadAddress)) // circulating supply
      );
    uint256 rewardsWithBooster = eligibleForRewardBooster(wallet)
      ? baseRewards.add(baseRewards.mul(getBoostMultiplier(wallet)).div(10**2))
      : baseRewards;
    return
      rewardsWithBooster > contractTokenBalance
        ? baseRewards
        : rewardsWithBooster;
  }

  function canClaimRewards(address user) public view returns (bool) {
    return
      block.timestamp > _rewardsLastClaim[user].add(rewardsClaimTimeSeconds) &&
      block.timestamp >
      _oklg.getLastETHRewardsClaim(user).add(rewardsClaimTimeSeconds);
  }

  function eligibleForRewardBooster(address wallet) public view returns (bool) {
    return
      boostRewardsContract != address(0) &&
      IConditional(boostRewardsContract).passesTest(wallet);
  }

  function resetLastClaim(address _user) external onlyOwner {
    _rewardsLastClaim[_user] = 0;
  }

  function claimETHRewards() external {
    require(
      _oklg.balanceOf(_msgSender()) > 0,
      'You must have a balance to claim ETH rewards'
    );
    require(
      canClaimRewards(_msgSender()),
      'Must wait claim period before claiming rewards'
    );
    _rewardsLastClaim[_msgSender()] = block.timestamp;

    uint256 rewardsSent = calculateETHRewards(_msgSender());
    payable(_msgSender()).call{ value: rewardsSent }('');
    emit SendETHRewards(_msgSender(), rewardsSent);
  }

  function claimTokenRewards(address token) external {
    require(
      _oklg.balanceOf(_msgSender()) > 0,
      'You must have a balance to claim rewards'
    );
    require(
      IERC20(token).balanceOf(address(this)) > 0,
      'We must have a token balance to claim rewards'
    );
    require(
      canClaimRewards(_msgSender()),
      'Must wait claim period before claiming rewards'
    );
    _rewardsLastClaim[_msgSender()] = block.timestamp;

    uint256 rewardsSent = calculateTokenRewards(_msgSender(), token);
    IERC20(token).transfer(_msgSender(), rewardsSent);
    emit SendTokenRewards(_msgSender(), token, rewardsSent);
  }

  // to recieve ETH from uniswapV2Router when swaping
  receive() external payable {}
}

// File: contracts/unaudited/OKLApeNewBuyerFeeExclusion.sol



// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '../interfaces/IConditional.sol';

contract OKLApeNewBuyerFeeExclusion is IConditional, Ownable {
  address public oklg;
  address public oklApe;
  uint256 public maxOKLGBalance = 10000 * 10**9;
  uint256 public minNFTBalance = 1;

  constructor(address _oklg, address _oklApe) {
    oklg = _oklg;
    oklApe = _oklApe;
  }

  function passesTest(address wallet) external view override returns (bool) {
    return
      wallet == address(0)
        ? false
        : IERC20(oklg).balanceOf(wallet) <= maxOKLGBalance &&
          IERC721(oklApe).balanceOf(wallet) >= minNFTBalance;
  }

  function setOKLGAddress(address _oklg) external onlyOwner {
    oklg = _oklg;
  }

  function setNFTAddress(address _oklApe) external onlyOwner {
    oklApe = _oklApe;
  }

  function setMaxOKLGBalance(uint256 _newMax) external onlyOwner {
    maxOKLGBalance = _newMax;
  }

  function setMinNFTBalance(uint256 _newMin) external onlyOwner {
    minNFTBalance = _newMin;
  }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)

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/token/ERC20/ERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/ERC20.sol)

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 Contracts guidelines: functions revert
 * instead 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 default 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");
        unchecked {
            _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");
        unchecked {
            _approve(_msgSender(), spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev Moves `amount` of tokens from `sender` to `recipient`.
     *
     * This 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");
        unchecked {
            _balances[sender] = senderBalance - amount;
        }
        _balances[recipient] += amount;

        emit Transfer(sender, recipient, amount);

        _afterTokenTransfer(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:
     *
     * - `account` 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);

        _afterTokenTransfer(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");
        unchecked {
            _balances[account] = accountBalance - amount;
        }
        _totalSupply -= amount;

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

        _afterTokenTransfer(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 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 {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been 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 _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (security/Pausable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    bool private _paused;

    /**
     * @dev Initializes the contract in unpaused state.
     */
    constructor() {
        _paused = false;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        require(!paused(), "Pausable: paused");
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        require(paused(), "Pausable: not paused");
        _;
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

// File: @openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/ERC721Pausable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../security/Pausable.sol";

/**
 * @dev ERC721 token with pausable token transfers, minting and burning.
 *
 * Useful for scenarios such as preventing trades until the end of an evaluation
 * period, or having an emergency switch for freezing all token transfers in the
 * event of a large bug.
 */
abstract contract ERC721Pausable is ERC721, Pausable {
    /**
     * @dev See {ERC721-_beforeTokenTransfer}.
     *
     * Requirements:
     *
     * - the contract must not be paused.
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);

        require(!paused(), "ERC721Pausable: token transfer while paused");
    }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/ERC721.sol)

pragma solidity ^0.8.0;

import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";

/**
 * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
 * the Metadata extension, but not including the Enumerable extension, which is available separately as
 * {ERC721Enumerable}.
 */
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
    using Address for address;
    using Strings for uint256;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping(address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping(uint256 => address) private _tokenApprovals;

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

    /**
     * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

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

    /**
     * @dev See {IERC721-balanceOf}.
     */
    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

    /**
     * @dev See {IERC721-ownerOf}.
     */
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: owner query for nonexistent token");
        return owner;
    }

    /**
     * @dev See {IERC721Metadata-name}.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC721Metadata-symbol}.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, can be overriden in child contracts.
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

    /**
     * @dev See {IERC721-approve}.
     */
    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(
            _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }

    /**
     * @dev See {IERC721-getApproved}.
     */
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");

        return _tokenApprovals[tokenId];
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

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

    /**
     * @dev See {IERC721-transferFrom}.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        _transfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        _safeTransfer(from, to, tokenId, _data);
    }

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * `_data` is additional data, it has no specified format and it is sent in call to `to`.
     *
     * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
     * implement alternative mechanisms to perform token transfer, such as signature-based.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeTransfer(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
        _transfer(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev Returns whether `tokenId` exists.
     *
     * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
     *
     * Tokens start existing when they are minted (`_mint`),
     * and stop existing when they are burned (`_burn`).
     */
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

    /**
     * @dev Returns whether `spender` is allowed to manage `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ERC721.ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }

    /**
     * @dev Safely mints `tokenId` and transfers it to `to`.
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }

    /**
     * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
     * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
     */
    function _safeMint(
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, _data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     *
     * Emits a {Transfer} event.
     */
    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    /**
     * @dev Destroys `tokenId`.
     * The approval is cleared when the token is burned.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     *
     * Emits a {Transfer} event.
     */
    function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId);

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *  As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     *
     * Emits a {Transfer} event.
     */
    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {
        require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
        require(to != address(0), "ERC721: transfer to the zero address");

        _beforeTokenTransfer(from, to, tokenId);

        // Clear approvals from the previous owner
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    /**
     * @dev Approve `to` to operate on `tokenId`
     *
     * Emits a {Approval} event.
     */
    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
    }

    /**
     * @dev Approve `operator` to operate on all of `owner` tokens
     *
     * Emits a {ApprovalForAll} event.
     */
    function _setApprovalForAll(
        address owner,
        address operator,
        bool approved
    ) internal virtual {
        require(owner != operator, "ERC721: approve to caller");
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }

    /**
     * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
     * The call is not executed if the target address is not a contract.
     *
     * @param from address representing the previous owner of the given token ID
     * @param to target address that will receive the tokens
     * @param tokenId uint256 ID of the token to be transferred
     * @param _data bytes optional data to send along with the call
     * @return bool whether the call correctly returned the expected magic value
     */
    function _checkOnERC721Received(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) private returns (bool) {
        if (to.isContract()) {
            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
                return retval == IERC721Receiver.onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ERC721: transfer to non ERC721Receiver implementer");
                } else {
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
     * transferred to `to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` 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 tokenId
    ) internal virtual {}
}

// File: @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721Receiver.sol)

pragma solidity ^0.8.0;

/**
 * @title ERC721 token receiver interface
 * @dev Interface for any contract that wants to support safeTransfers
 * from ERC721 asset contracts.
 */
interface IERC721Receiver {
    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
     *
     * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Metadata is IERC721 {
    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

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

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Address.sol)

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;
        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");

        (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");

        (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");

        (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");

        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal 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

                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol)

pragma solidity ^0.8.0;

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)

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/token/ERC721/extensions/ERC721Enumerable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/ERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "./IERC721Enumerable.sol";

/**
 * @dev This implements an optional extension of {ERC721} defined in the EIP that adds
 * enumerability of all the token ids in the contract as well as all token ids owned by each
 * account.
 */
abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {
    // Mapping from owner to list of owned token IDs
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

    // Mapping from token ID to index of the owner tokens list
    mapping(uint256 => uint256) private _ownedTokensIndex;

    // Array with all token ids, used for enumeration
    uint256[] private _allTokens;

    // Mapping from token id to position in the allTokens array
    mapping(uint256 => uint256) private _allTokensIndex;

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

    /**
     * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
        return _ownedTokens[owner][index];
    }

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

    /**
     * @dev See {IERC721Enumerable-tokenByIndex}.
     */
    function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds");
        return _allTokens[index];
    }

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

        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    /**
     * @dev Private function to add a token to this extension's ownership-tracking data structures.
     * @param to address representing the new owner of the given token ID
     * @param tokenId uint256 ID of the token to be added to the tokens list of the given address
     */
    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    /**
     * @dev Private function to add a token to this extension's token tracking data structures.
     * @param tokenId uint256 ID of the token to be added to the tokens list
     */
    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    /**
     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
     * @param from address representing the previous owner of the given token ID
     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
     */
    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

// File: contracts/OKLGTokenLocker.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import './OKLGProduct.sol';

/**
 * @title MTGYTokenLocker
 * @dev This is the main contract that supports locking/vesting tokens.
 */
contract MTGYTokenLocker is OKLGProduct {
  using SafeMath for uint48;
  using SafeMath for uint256;

  struct Locker {
    address owner;
    address token;
    bool isNft; // rewardToken is either ERC20 or ERC721
    uint256 amountSupply; // If ERC-721, will always be 1, otherwise is amount of tokens locked
    uint256 tokenId; // only populated if isNft is true
    uint48 start; // timestamp (uint256) of start lock time (block.timestamp at creation)
    uint48 end; // timestamp (uint256) of end lock time
    address[] withdrawable; // any additional addresses that can withdraw tokens from this locker
    uint256 amountWithdrawn;
    // numberVests:
    // 1 means can only withdraw tokens at end of lock period
    // any other number is evenly distributed throughout lock period
    uint8 numberVests;
  }

  mapping(address => uint16[]) public lockersByOwner;
  mapping(address => uint16[]) public lockersByToken;
  mapping(address => uint16[]) public lockersByWithdrawable;
  Locker[] public lockers;

  event CreateLocker(address indexed creator, uint256 idx);
  event WithdrawTokens(
    uint256 indexed idx,
    address withdrawer,
    uint256 numTokensOrTokenId
  );

  constructor(address _tokenAddress, address _spendAddress)
    OKLGProduct(uint8(5), _tokenAddress, _spendAddress)
  {}

  function getAllLockers() external view returns (Locker[] memory) {
    return lockers;
  }

  function createLocker(
    address _tokenAddress,
    uint256 _amountOrTokenId,
    uint48 _end,
    uint8 _numberVests,
    address[] memory _withdrawableAddresses,
    bool _isNft
  ) external payable {
    require(
      _end > block.timestamp,
      'Locker end date must be after current time.'
    );

    _payForService(0);

    if (_isNft) {
      IERC721 _token = IERC721(_tokenAddress);
      _token.transferFrom(msg.sender, address(this), _amountOrTokenId);
    } else {
      IERC20 _token = IERC20(_tokenAddress);
      _token.transferFrom(msg.sender, address(this), _amountOrTokenId);
    }

    lockers.push(
      Locker({
        owner: msg.sender,
        isNft: _isNft,
        token: _tokenAddress,
        amountSupply: _isNft ? 1 : _amountOrTokenId,
        tokenId: _isNft ? _amountOrTokenId : 0,
        start: uint48(block.timestamp),
        end: _end,
        withdrawable: _withdrawableAddresses,
        amountWithdrawn: 0,
        numberVests: _isNft ? 1 : (_numberVests == 0 ? 1 : _numberVests)
      })
    );
    uint16 _newIdx = uint16(lockers.length - 1);
    lockersByOwner[msg.sender].push(_newIdx);
    lockersByToken[_tokenAddress].push(_newIdx);
    if (_withdrawableAddresses.length > 0) {
      for (uint16 _i = 0; _i < _withdrawableAddresses.length; _i++) {
        lockersByWithdrawable[_withdrawableAddresses[_i]].push(_newIdx);
      }
    }
    emit CreateLocker(msg.sender, _newIdx);
  }

  function withdrawLockedTokens(uint16 _idx, uint256 _amountOrTokenId)
    external
  {
    Locker storage _locker = lockers[_idx];
    require(
      _locker.amountWithdrawn < _locker.amountSupply,
      'All tokens have been withdrawn from this locker.'
    );

    bool _canWithdraw = msg.sender == _locker.owner;
    if (!_canWithdraw) {
      for (uint256 _i = 0; _i < _locker.withdrawable.length; _i++) {
        if (_locker.withdrawable[_i] == msg.sender) {
          _canWithdraw = true;
          break;
        }
      }
    }
    require(_canWithdraw, 'Must be locker owner or a withdrawable wallet.');

    if (_locker.isNft) {
      IERC721 _token = IERC721(_locker.token);
      _token.transferFrom(address(this), msg.sender, _amountOrTokenId);
    } else {
      uint256 _maxAmount = maxWithdrawableTokens(_idx);
      require(
        _amountOrTokenId > 0 &&
          _maxAmount > 0 &&
          _maxAmount <= _amountOrTokenId,
        'Make sure you enter a valid withdrawable amount and not more than has vested.'
      );
      IERC20 _token = IERC20(_locker.token);
      _token.transferFrom(address(this), msg.sender, _amountOrTokenId);
    }
    _locker.amountWithdrawn = _locker.isNft ? 1 : _amountOrTokenId;
    emit WithdrawTokens(_idx, msg.sender, _amountOrTokenId);
  }

  function changeLockerOwner(uint16 _idx, address _newOwner) external {
    Locker storage _locker = lockers[_idx];
    require(
      _locker.owner == msg.sender,
      'Must be the locker owner to change owner.'
    );
    _locker.owner = _newOwner;
  }

  function changeLockerEndTime(uint16 _idx, uint48 _newEnd) external {
    Locker storage _locker = lockers[_idx];
    require(
      _locker.owner == msg.sender,
      'Must be the locker owner to change owner.'
    );
    require(_newEnd > _locker.end, 'Can only extend end time, not shorten it.');
    _locker.end = _newEnd;
  }

  function maxWithdrawableTokens(uint16 _idx) public view returns (uint256) {
    Locker memory _locker = lockers[_idx];
    uint256 _fullLockPeriodSec = _locker.end.sub(_locker.start);
    uint256 _secondsPerVest = _fullLockPeriodSec.div(_locker.numberVests);
    uint256 _tokensPerVest = _locker.amountSupply.div(_locker.numberVests);
    uint256 _numberWithdrawableVests = (block.timestamp.sub(_locker.start)).div(
      _secondsPerVest
    );
    if (_numberWithdrawableVests == 0) return 0;
    return
      _numberWithdrawableVests.mul(_tokensPerVest).sub(_locker.amountWithdrawn);
  }
}

// File: contracts/OKLGProduct.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './interfaces/IOKLGSpend.sol';
import './OKLGWithdrawable.sol';

/**
 * @title OKLGProduct
 * @dev Contract that every product developed in the OKLG ecosystem should implement
 */
contract OKLGProduct is OKLGWithdrawable {
  IERC20 private _token; // OKLG
  IOKLGSpend private _spend;

  uint8 public productID;

  constructor(
    uint8 _productID,
    address _tokenAddy,
    address _spendAddy
  ) {
    productID = _productID;
    _token = IERC20(_tokenAddy);
    _spend = IOKLGSpend(_spendAddy);
  }

  function setTokenAddy(address _tokenAddy) external onlyOwner {
    _token = IERC20(_tokenAddy);
  }

  function setSpendAddy(address _spendAddy) external onlyOwner {
    _spend = IOKLGSpend(_spendAddy);
  }

  function setProductID(uint8 _newId) external onlyOwner {
    productID = _newId;
  }

  function getTokenAddress() public view returns (address) {
    return address(_token);
  }

  function getSpendAddress() public view returns (address) {
    return address(_spend);
  }

  function _payForService(uint256 _weiToRemoveFromSpend) internal {
    _spend.spendOnProduct{ value: msg.value - _weiToRemoveFromSpend }(
      msg.sender,
      productID
    );
  }
}

// File: contracts/interfaces/IOKLGSpend.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/**
 * @title IOKLGSpend
 * @dev Logic for spending OKLG on products in the product ecosystem.
 */
interface IOKLGSpend {
  function spendOnProduct(address _payor, uint8 _product) external payable;
}

// File: contracts/OKLGTrustedTimestamping.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './OKLGProduct.sol';

/**
 * @title OKLGTrustedTimestamping
 * @dev Stores SHA256 data hashes for trusted timestamping implementations.
 */
contract OKLGTrustedTimestamping is OKLGProduct {
  struct DataHash {
    bytes32 dataHash;
    uint256 time;
    string fileName;
    uint256 fileSizeBytes;
  }

  struct Address {
    address addy;
    uint256 time;
  }

  uint256 public totalNumberHashesStored;
  mapping(address => DataHash[]) public addressHashes;
  mapping(bytes32 => Address[]) public fileHashesToAddress;

  event StoreHash(address from, bytes32 dataHash);

  constructor(address _tokenAddress, address _pendAddress)
    OKLGProduct(uint8(3), _tokenAddress, _pendAddress)
  {}

  /**
   * @dev Process transaction and store hash in blockchain
   */
  function storeHash(
    bytes32 dataHash,
    string memory fileName,
    uint256 fileSizeBytes
  ) external payable {
    _payForService(0);

    uint256 theTimeNow = block.timestamp;
    addressHashes[msg.sender].push(
      DataHash({
        dataHash: dataHash,
        time: theTimeNow,
        fileName: fileName,
        fileSizeBytes: fileSizeBytes
      })
    );
    fileHashesToAddress[dataHash].push(
      Address({ addy: msg.sender, time: theTimeNow })
    );
    totalNumberHashesStored++;
    emit StoreHash(msg.sender, dataHash);
  }

  function getHashesForAddress(address _userAddy)
    external
    view
    returns (DataHash[] memory)
  {
    return addressHashes[_userAddy];
  }

  function getAddressesForHash(bytes32 dataHash)
    external
    view
    returns (Address[] memory)
  {
    return fileHashesToAddress[dataHash];
  }
}

// File: contracts/OKLGRaffle.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import './OKLGProduct.sol';

/**
 * @title OKLGRaffle
 * @dev This is the main contract that supports lotteries and raffles.
 */
contract OKLGRaffle is OKLGProduct {
  struct Raffle {
    address owner;
    bool isNft; // rewardToken is either ERC20 or ERC721
    address rewardToken;
    uint256 rewardAmountOrTokenId;
    uint256 start; // timestamp (uint256) of start time (0 if start when raffle is created)
    uint256 end; // timestamp (uint256) of end time (0 if can be entered until owner draws)
    address entryToken; // ERC20 token requiring user to send to enter
    uint256 entryFee; // ERC20 num tokens user must send to enter, or 0 if no entry fee
    uint256 entryFeesCollected; // amount of fees collected by entries and paid to raffle/lottery owner
    uint256 maxEntriesPerAddress; // 0 means unlimited entries
    address[] entries;
    address winner;
    bool isComplete;
    bool isClosed;
  }

  uint8 public entryFeePercentageCharge = 2;

  mapping(bytes32 => Raffle) public raffles;
  bytes32[] public raffleIds;
  mapping(bytes32 => mapping(address => uint256)) public entriesIndexed;

  event CreateRaffle(address indexed creator, bytes32 id);
  event EnterRaffle(
    bytes32 indexed id,
    address raffler,
    uint256 numberOfEntries
  );
  event DrawWinner(bytes32 indexed id, address winner, uint256 amount);
  event CloseRaffle(bytes32 indexed id);

  constructor(address _tokenAddress, address _spendAddress)
    OKLGProduct(uint8(4), _tokenAddress, _spendAddress)
  {}

  function getAllRaffles() external view returns (bytes32[] memory) {
    return raffleIds;
  }

  function getRaffleEntries(bytes32 _id)
    external
    view
    returns (address[] memory)
  {
    return raffles[_id].entries;
  }

  function createRaffle(
    address _rewardTokenAddress,
    uint256 _rewardAmountOrTokenId,
    bool _isNft,
    uint256 _start,
    uint256 _end,
    address _entryToken,
    uint256 _entryFee,
    uint256 _maxEntriesPerAddress
  ) external payable {
    _validateDates(_start, _end);
    _payForService(0);

    if (_isNft) {
      IERC721 _rewardToken = IERC721(_rewardTokenAddress);
      _rewardToken.transferFrom(
        msg.sender,
        address(this),
        _rewardAmountOrTokenId
      );
    } else {
      IERC20 _rewardToken = IERC20(_rewardTokenAddress);
      _rewardToken.transferFrom(
        msg.sender,
        address(this),
        _rewardAmountOrTokenId
      );
    }

    bytes32 _id = sha256(abi.encodePacked(msg.sender, block.number));
    address[] memory _entries;
    raffles[_id] = Raffle({
      owner: msg.sender,
      isNft: _isNft,
      rewardToken: _rewardTokenAddress,
      rewardAmountOrTokenId: _rewardAmountOrTokenId,
      start: _start,
      end: _end,
      entryToken: _entryToken,
      entryFee: _entryFee,
      entryFeesCollected: 0,
      maxEntriesPerAddress: _maxEntriesPerAddress,
      entries: _entries,
      winner: address(0),
      isComplete: false,
      isClosed: false
    });
    raffleIds.push(_id);
    emit CreateRaffle(msg.sender, _id);
  }

  function drawWinner(bytes32 _id) external {
    Raffle storage _raffle = raffles[_id];
    require(
      _raffle.end == 0 || block.timestamp > _raffle.end,
      'Raffle entry period is not over yet.'
    );
    require(
      !_raffle.isComplete,
      'Raffle has already been drawn and completed.'
    );

    if (_raffle.entryFeesCollected > 0) {
      IERC20 _entryToken = IERC20(_raffle.entryToken);
      uint256 _feesToSendOwner = _raffle.entryFeesCollected;
      if (entryFeePercentageCharge > 0) {
        uint256 _feeChargeAmount = (_feesToSendOwner *
          entryFeePercentageCharge) / 100;
        _entryToken.transfer(owner(), _feeChargeAmount);
        _feesToSendOwner -= _feeChargeAmount;
      }
      _entryToken.transfer(_raffle.owner, _feesToSendOwner);
    }

    uint256 _winnerIdx = _random(_raffle.entries.length) %
      _raffle.entries.length;
    address _winner = _raffle.entries[_winnerIdx];
    _raffle.winner = _winner;

    if (_raffle.isNft) {
      IERC721 _rewardToken = IERC721(_raffle.rewardToken);
      _rewardToken.transferFrom(
        address(this),
        _winner,
        _raffle.rewardAmountOrTokenId
      );
    } else {
      IERC20 _rewardToken = IERC20(_raffle.rewardToken);
      _rewardToken.transfer(_winner, _raffle.rewardAmountOrTokenId);
    }

    _raffle.isComplete = true;
    emit DrawWinner(_id, _winner, _raffle.rewardAmountOrTokenId);
  }

  function closeRaffleAndRefund(bytes32 _id) external {
    Raffle storage _raffle = raffles[_id];
    require(
      _raffle.owner == msg.sender,
      'Must be the raffle owner to draw winner.'
    );
    require(
      !_raffle.isComplete,
      'Raffle cannot be closed if it is completed already.'
    );

    IERC20 _entryToken = IERC20(_raffle.entryToken);
    for (uint256 _i = 0; _i < _raffle.entries.length; _i++) {
      address _user = _raffle.entries[_i];
      _entryToken.transfer(_user, _raffle.entryFee);
    }

    if (_raffle.isNft) {
      IERC721 _rewardToken = IERC721(_raffle.rewardToken);
      _rewardToken.transferFrom(
        address(this),
        msg.sender,
        _raffle.rewardAmountOrTokenId
      );
    } else {
      IERC20 _rewardToken = IERC20(_raffle.rewardToken);
      _rewardToken.transfer(msg.sender, _raffle.rewardAmountOrTokenId);
    }

    _raffle.isComplete = true;
    _raffle.isClosed = true;
    emit CloseRaffle(_id);
  }

  function enterRaffle(bytes32 _id, uint256 _numEntries) external {
    Raffle storage _raffle = raffles[_id];
    require(_raffle.owner != address(0), 'We do not recognize this raffle.');
    require(
      _raffle.start <= block.timestamp,
      'It must be after the start time to enter the raffle.'
    );
    require(
      _raffle.end == 0 || _raffle.end >= block.timestamp,
      'It must be before the end time to enter the raffle.'
    );
    require(
      _numEntries > 0 &&
        (_raffle.maxEntriesPerAddress == 0 ||
          entriesIndexed[_id][msg.sender] + _numEntries <=
          _raffle.maxEntriesPerAddress),
      'You have entered the maximum number of times you are allowed.'
    );
    require(!_raffle.isComplete, 'Raffle cannot be complete to be entered.');

    if (_raffle.entryFee > 0) {
      IERC20 _entryToken = IERC20(_raffle.entryToken);
      _entryToken.transferFrom(
        msg.sender,
        address(this),
        _raffle.entryFee * _numEntries
      );
      _raffle.entryFeesCollected += _raffle.entryFee * _numEntries;
    }

    for (uint256 _i = 0; _i < _numEntries; _i++) {
      _raffle.entries.push(msg.sender);
    }
    entriesIndexed[_id][msg.sender] += _numEntries;
    emit EnterRaffle(_id, msg.sender, _numEntries);
  }

  function changeRaffleOwner(bytes32 _id, address _newOwner) external {
    Raffle storage _raffle = raffles[_id];
    require(
      _raffle.owner == msg.sender,
      'Must be the raffle owner to change owner.'
    );
    require(
      !_raffle.isComplete,
      'Raffle has already been drawn and completed.'
    );

    _raffle.owner = _newOwner;
  }

  function changeEndDate(bytes32 _id, uint256 _newEnd) external {
    Raffle storage _raffle = raffles[_id];
    require(
      _raffle.owner == msg.sender,
      'Must be the raffle owner to change owner.'
    );
    require(
      !_raffle.isComplete,
      'Raffle has already been drawn and completed.'
    );

    _raffle.end = _newEnd;
  }

  function changeEntryFeePercentageCharge(uint8 _newPercentage)
    external
    onlyOwner
  {
    require(
      _newPercentage >= 0 && _newPercentage < 100,
      'Should be between 0 and 100.'
    );
    entryFeePercentageCharge = _newPercentage;
  }

  function _validateDates(uint256 _start, uint256 _end) private view {
    require(
      _start == 0 || _start >= block.timestamp,
      'start time should be 0 or after the current time'
    );
    require(
      _end == 0 || _end > block.timestamp,
      'end time should be 0 or after the current time'
    );
    if (_start > 0) {
      if (_end > 0) {
        require(_start < _end, 'start time must be before end time');
      }
    }
  }

  function _random(uint256 _entries) private view returns (uint256) {
    return
      uint256(
        keccak256(abi.encodePacked(block.difficulty, block.timestamp, _entries))
      );
  }
}

// File: contracts/OKLGPasswordManager.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import './OKLGProduct.sol';

/**
 * @title OKLGPasswordManager
 * @dev Logic for storing and retrieving account information from the blockchain.
 */
contract OKLGPasswordManager is OKLGProduct {
  using SafeMath for uint256;

  struct AccountInfo {
    string id;
    uint256 timestamp;
    string iv;
    string ciphertext;
    bool isDeleted;
  }

  // the normal mapping of all accounts owned by a user
  mapping(address => AccountInfo[]) public userAccounts;

  constructor(address _tokenAddress, address _spendAddress)
    OKLGProduct(uint8(2), _tokenAddress, _spendAddress)
  {}

  function getAllAccounts(address _userAddy)
    external
    view
    returns (AccountInfo[] memory)
  {
    return userAccounts[_userAddy];
  }

  function getAccountById(string memory _id)
    external
    view
    returns (AccountInfo memory)
  {
    AccountInfo[] memory _userInfo = userAccounts[msg.sender];
    for (uint256 _i = 0; _i < _userInfo.length; _i++) {
      if (_compareStr(_userInfo[_i].id, _id)) {
        return _userInfo[_i];
      }
    }
    return
      AccountInfo({
        id: '',
        timestamp: 0,
        iv: '',
        ciphertext: '',
        isDeleted: false
      });
  }

  function updateAccountById(
    string memory _id,
    string memory _newIv,
    string memory _newAccountData
  ) external returns (bool) {
    AccountInfo[] memory _userInfo = userAccounts[msg.sender];
    for (uint256 _i = 0; _i < _userInfo.length; _i++) {
      if (_compareStr(_userInfo[_i].id, _id)) {
        userAccounts[msg.sender][_i].iv = _newIv;
        userAccounts[msg.sender][_i].timestamp = block.timestamp;
        userAccounts[msg.sender][_i].ciphertext = _newAccountData;
        return true;
      }
    }
    return false;
  }

  function addAccount(
    string memory _id,
    string memory _iv,
    string memory _ciphertext
  ) external payable {
    _payForService(0);

    userAccounts[msg.sender].push(
      AccountInfo({
        id: _id,
        timestamp: block.timestamp,
        iv: _iv,
        ciphertext: _ciphertext,
        isDeleted: false
      })
    );
  }

  function bulkAddAccounts(AccountInfo[] memory accounts) external payable {
    require(
      accounts.length >= 5,
      'you need a minimum of 5 accounts to add in bulk at a 50% discount service cost'
    );
    _payForService(0);

    for (uint256 _i = 0; _i < accounts.length; _i++) {
      AccountInfo memory _account = accounts[_i];
      userAccounts[msg.sender].push(
        AccountInfo({
          id: _account.id,
          timestamp: block.timestamp,
          iv: _account.iv,
          ciphertext: _account.ciphertext,
          isDeleted: false
        })
      );
    }
  }

  function deleteAccount(string memory _id) external returns (bool) {
    AccountInfo[] memory _userInfo = userAccounts[msg.sender];
    for (uint256 _i = 0; _i < _userInfo.length; _i++) {
      if (_compareStr(_userInfo[_i].id, _id)) {
        userAccounts[msg.sender][_i].timestamp = block.timestamp;
        userAccounts[msg.sender][_i].isDeleted = true;
        return true;
      }
    }
    return false;
  }

  function _compareStr(string memory a, string memory b)
    private
    pure
    returns (bool)
  {
    return (keccak256(abi.encodePacked((a))) ==
      keccak256(abi.encodePacked((b))));
  }
}

// File: contracts/OKLGFaaSToken.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';

/**
 * @title OKLGFaaSToken (sOKLG)
 * @notice Represents a contract where a token owner has put her tokens up for others to stake and earn said tokens.
 */
contract OKLGFaaSToken is ERC20 {
  using SafeMath for uint256;
  bool public contractIsRemoved = false;

  IERC20 private _rewardsToken;
  IERC20 private _stakedERC20;
  IERC721 private _stakedERC721;
  PoolInfo public pool;
  address private constant _burner = 0x000000000000000000000000000000000000dEaD;

  struct PoolInfo {
    address creator; // address of contract creator
    address tokenOwner; // address of original rewards token owner
    uint256 origTotSupply; // supply of rewards tokens put up to be rewarded by original owner
    uint256 curRewardsSupply; // current supply of rewards
    uint256 totalTokensStaked; // current amount of tokens staked
    uint256 creationBlock; // block this contract was created
    uint256 perBlockNum; // amount of rewards tokens rewarded per block
    uint256 lockedUntilDate; // unix timestamp of how long this contract is locked and can't be changed
    // uint256 allocPoint; // How many allocation points assigned to this pool. ERC20s to distribute per block.
    uint256 lastRewardBlock; // Last block number that ERC20s distribution occurs.
    uint256 accERC20PerShare; // Accumulated ERC20s per share, times 1e36.
    uint256 stakeTimeLockSec; // number of seconds after depositing the user is required to stake before unstaking
    bool isStakedNft;
  }

  struct StakerInfo {
    uint256 blockOriginallyStaked; // block the user originally staked
    uint256 timeOriginallyStaked; // unix timestamp in seconds that the user originally staked
    uint256 blockLastHarvested; // the block the user last claimed/harvested rewards
    uint256 rewardDebt; // Reward debt. See explanation below.
    uint256[] nftTokenIds; // if this is an NFT staking pool, make sure we store the token IDs here
  }

  struct BlockTokenTotal {
    uint256 blockNumber;
    uint256 totalTokens;
  }

  // mapping of userAddresses => tokenAddresses that can
  // can be evaluated to determine for a particular user which tokens
  // they are staking.
  mapping(address => StakerInfo) public stakers;

  event Deposit(address indexed user, uint256 amount);
  event Withdraw(address indexed user, uint256 amount);

  /**
   * @notice The constructor for the Staking Token.
   * @param _name Name of the staking token
   * @param _symbol Name of the staking token symbol
   * @param _rewardSupply The amount of tokens to mint on construction, this should be the same as the tokens provided by the creating user.
   * @param _rewardsTokenAddy Contract address of token to be rewarded to users
   * @param _stakedTokenAddy Contract address of token to be staked by users
   * @param _originalTokenOwner Address of user putting up staking tokens to be staked
   * @param _perBlockAmount Amount of tokens to be rewarded per block
   * @param _lockedUntilDate Unix timestamp that the staked tokens will be locked. 0 means locked forever until all tokens are staked
   * @param _stakeTimeLockSec number of seconds a user is required to stake, or 0 if none
   * @param _isStakedNft is this an NFT staking pool
   */
  constructor(
    string memory _name,
    string memory _symbol,
    uint256 _rewardSupply,
    address _rewardsTokenAddy,
    address _stakedTokenAddy,
    address _originalTokenOwner,
    uint256 _perBlockAmount,
    uint256 _lockedUntilDate,
    uint256 _stakeTimeLockSec,
    bool _isStakedNft
  ) ERC20(_name, _symbol) {
    require(
      _perBlockAmount > uint256(0) && _perBlockAmount <= uint256(_rewardSupply),
      'per block amount must be more than 0 and less than supply'
    );

    // A locked date of '0' corresponds to being locked forever until the supply has expired and been rewards to all stakers
    require(
      _lockedUntilDate > block.timestamp || _lockedUntilDate == 0,
      'locked time must be after now or 0'
    );

    _rewardsToken = IERC20(_rewardsTokenAddy);
    if (_isStakedNft) {
      _stakedERC721 = IERC721(_stakedTokenAddy);
    } else {
      _stakedERC20 = IERC20(_stakedTokenAddy);
    }

    pool = PoolInfo({
      creator: msg.sender,
      tokenOwner: _originalTokenOwner,
      origTotSupply: _rewardSupply,
      curRewardsSupply: _rewardSupply,
      totalTokensStaked: 0,
      creationBlock: 0,
      perBlockNum: _perBlockAmount,
      lockedUntilDate: _lockedUntilDate,
      lastRewardBlock: block.number,
      accERC20PerShare: 0,
      stakeTimeLockSec: _stakeTimeLockSec,
      isStakedNft: _isStakedNft
    });
  }

  // SHOULD ONLY BE CALLED AT CONTRACT CREATION and allows changing
  // the initial supply if tokenomics of token transfer causes
  // the original staking contract supply to be less than the original
  function updateSupply(uint256 _newSupply) external {
    require(
      msg.sender == pool.creator,
      'only contract creator can update the supply'
    );
    pool.origTotSupply = _newSupply;
    pool.curRewardsSupply = _newSupply;
  }

  function stakedTokenAddress() external view returns (address) {
    return pool.isStakedNft ? address(_stakedERC721) : address(_stakedERC20);
  }

  function rewardsTokenAddress() external view returns (address) {
    return address(_rewardsToken);
  }

  function tokenOwner() external view returns (address) {
    return pool.tokenOwner;
  }

  function getLockedUntilDate() external view returns (uint256) {
    return pool.lockedUntilDate;
  }

  function removeStakeableTokens() external {
    require(
      msg.sender == pool.creator || msg.sender == pool.tokenOwner,
      'caller must be the contract creator or owner to remove stakable tokens'
    );
    _rewardsToken.transfer(pool.tokenOwner, pool.curRewardsSupply);
    pool.curRewardsSupply = 0;
    contractIsRemoved = true;
  }

  // function updateTimestamp(uint256 _newTime) external {
  //   require(
  //     msg.sender == tokenOwner,
  //     'updateTimestamp user must be original token owner'
  //   );
  //   require(
  //     _newTime > lockedUntilDate || _newTime == 0,
  //     'you cannot change timestamp if it is before the locked time or was set to be locked forever'
  //   );
  //   lockedUntilDate = _newTime;
  // }

  function stakeTokens(uint256 _amount, uint256[] memory _tokenIds) public {
    require(
      getLastStakableBlock() > block.number,
      'this farm is expired and no more stakers can be added'
    );

    _updatePool();

    if (balanceOf(msg.sender) > 0) {
      _harvestTokens(msg.sender);
    }

    uint256 _finalAmountTransferred;
    if (pool.isStakedNft) {
      require(
        _tokenIds.length > 0,
        "you need to provide NFT token IDs you're staking"
      );
      for (uint256 _i = 0; _i < _tokenIds.length; _i++) {
        _stakedERC721.transferFrom(msg.sender, address(this), _tokenIds[_i]);
      }

      _finalAmountTransferred = _tokenIds.length;
    } else {
      uint256 _contractBalanceBefore = _stakedERC20.balanceOf(address(this));
      _stakedERC20.transferFrom(msg.sender, address(this), _amount);

      // in the event a token contract on transfer taxes, burns, etc. tokens
      // the contract might not get the entire amount that the user originally
      // transferred. Need to calculate from the previous contract balance
      // so we know how many were actually transferred.
      _finalAmountTransferred = _stakedERC20.balanceOf(address(this)).sub(
        _contractBalanceBefore
      );
    }

    if (totalSupply() == 0) {
      pool.creationBlock = block.number;
      pool.lastRewardBlock = block.number;
    }
    _mint(msg.sender, _finalAmountTransferred);
    StakerInfo storage _staker = stakers[msg.sender];
    _staker.blockOriginallyStaked = block.number;
    _staker.timeOriginallyStaked = block.timestamp;
    _staker.blockLastHarvested = block.number;
    _staker.rewardDebt = balanceOf(msg.sender).mul(pool.accERC20PerShare).div(
      1e36
    );
    for (uint256 _i = 0; _i < _tokenIds.length; _i++) {
      _staker.nftTokenIds.push(_tokenIds[_i]);
    }
    _updNumStaked(_finalAmountTransferred, 'add');
    emit Deposit(msg.sender, _finalAmountTransferred);
  }

  // pass 'false' for _shouldHarvest for emergency unstaking without claiming rewards
  function unstakeTokens(uint256 _amount, bool _shouldHarvest) external {
    StakerInfo memory _staker = stakers[msg.sender];
    uint256 _userBalance = balanceOf(msg.sender);
    require(
      pool.isStakedNft ? true : _amount <= _userBalance,
      'user can only unstake amount they have currently staked or less'
    );

    // allow unstaking if the user is emergency unstaking and not getting rewards or
    // if theres a time lock that it's past the time lock or
    // the contract rewards were removed by the original contract creator or
    // the contract is expired
    require(
      !_shouldHarvest ||
        block.timestamp >=
        _staker.timeOriginallyStaked.add(pool.stakeTimeLockSec) ||
        contractIsRemoved ||
        block.number > getLastStakableBlock(),
      'you have not staked for minimum time lock yet and the pool is not expired'
    );

    _updatePool();

    if (_shouldHarvest) {
      _harvestTokens(msg.sender);
    }

    uint256 _amountToRemoveFromStaked = pool.isStakedNft
      ? _userBalance
      : _amount;
    transfer(_burner, _amountToRemoveFromStaked);
    if (pool.isStakedNft) {
      for (uint256 _i = 0; _i < _staker.nftTokenIds.length; _i++) {
        _stakedERC721.transferFrom(
          address(this),
          msg.sender,
          _staker.nftTokenIds[_i]
        );
      }
    } else {
      require(
        _stakedERC20.transfer(msg.sender, _amountToRemoveFromStaked),
        'unable to send user original tokens'
      );
    }

    if (balanceOf(msg.sender) <= 0) {
      delete stakers[msg.sender];
    }
    _updNumStaked(_amountToRemoveFromStaked, 'remove');
    emit Withdraw(msg.sender, _amountToRemoveFromStaked);
  }

  function emergencyUnstake() external {
    StakerInfo memory _staker = stakers[msg.sender];
    uint256 _amountToRemoveFromStaked = balanceOf(msg.sender);
    require(
      _amountToRemoveFromStaked > 0,
      'user can only unstake if they have tokens in the pool'
    );

    transfer(_burner, _amountToRemoveFromStaked);
    if (pool.isStakedNft) {
      for (uint256 _i = 0; _i < _staker.nftTokenIds.length; _i++) {
        _stakedERC721.transferFrom(
          address(this),
          msg.sender,
          _staker.nftTokenIds[_i]
        );
      }
    } else {
      require(
        _stakedERC20.transfer(msg.sender, _amountToRemoveFromStaked),
        'unable to send user original tokens'
      );
    }

    delete stakers[msg.sender];
    _updNumStaked(_amountToRemoveFromStaked, 'remove');
    emit Withdraw(msg.sender, _amountToRemoveFromStaked);
  }

  function harvestForUser(address _userAddy, bool _autoCompound)
    external
    returns (uint256)
  {
    require(
      msg.sender == pool.creator || msg.sender == _userAddy,
      'can only harvest tokens for someone else if this was the contract creator'
    );
    _updatePool();
    uint256 _tokensToUser = _harvestTokens(_userAddy);

    if (
      _autoCompound &&
      !pool.isStakedNft &&
      address(_rewardsToken) == address(_stakedERC20)
    ) {
      uint256[] memory _placeholder;
      stakeTokens(_tokensToUser, _placeholder);
    }

    return _tokensToUser;
  }

  function getLastStakableBlock() public view returns (uint256) {
    uint256 _blockToAdd = pool.creationBlock == 0
      ? block.number
      : pool.creationBlock;
    return pool.origTotSupply.div(pool.perBlockNum).add(_blockToAdd);
  }

  function calcHarvestTot(address _userAddy) public view returns (uint256) {
    StakerInfo memory _staker = stakers[_userAddy];

    if (
      _staker.blockLastHarvested >= block.number ||
      _staker.blockOriginallyStaked == 0 ||
      pool.totalTokensStaked == 0
    ) {
      return uint256(0);
    }

    uint256 _accERC20PerShare = pool.accERC20PerShare;

    if (block.number > pool.lastRewardBlock && pool.totalTokensStaked != 0) {
      uint256 _endBlock = getLastStakableBlock();
      uint256 _lastBlock = block.number < _endBlock ? block.number : _endBlock;
      uint256 _nrOfBlocks = _lastBlock.sub(pool.lastRewardBlock);
      uint256 _erc20Reward = _nrOfBlocks.mul(pool.perBlockNum);
      _accERC20PerShare = _accERC20PerShare.add(
        _erc20Reward.mul(1e36).div(pool.totalTokensStaked)
      );
    }

    return
      balanceOf(_userAddy).mul(_accERC20PerShare).div(1e36).sub(
        _staker.rewardDebt
      );
  }

  // Update reward variables of the given pool to be up-to-date.
  function _updatePool() private {
    uint256 _endBlock = getLastStakableBlock();
    uint256 _lastBlock = block.number < _endBlock ? block.number : _endBlock;

    if (_lastBlock <= pool.lastRewardBlock) {
      return;
    }
    uint256 _stakedSupply = pool.totalTokensStaked;
    if (_stakedSupply == 0) {
      pool.lastRewardBlock = _lastBlock;
      return;
    }

    uint256 _nrOfBlocks = _lastBlock.sub(pool.lastRewardBlock);
    uint256 _erc20Reward = _nrOfBlocks.mul(pool.perBlockNum);

    pool.accERC20PerShare = pool.accERC20PerShare.add(
      _erc20Reward.mul(1e36).div(_stakedSupply)
    );
    pool.lastRewardBlock = _lastBlock;
  }

  function _harvestTokens(address _userAddy) private returns (uint256) {
    StakerInfo storage _staker = stakers[_userAddy];
    require(_staker.blockOriginallyStaked > 0, 'user must have tokens staked');

    uint256 _num2Trans = calcHarvestTot(_userAddy);
    if (_num2Trans > 0) {
      require(
        _rewardsToken.transfer(_userAddy, _num2Trans),
        'unable to send user their harvested tokens'
      );
      pool.curRewardsSupply = pool.curRewardsSupply.sub(_num2Trans);
    }
    _staker.rewardDebt = balanceOf(_userAddy).mul(pool.accERC20PerShare).div(
      1e36
    );
    _staker.blockLastHarvested = block.number;
    return _num2Trans;
  }

  // update the amount currently staked after a user harvests
  function _updNumStaked(uint256 _amount, string memory _operation) private {
    if (_compareStr(_operation, 'remove')) {
      pool.totalTokensStaked = pool.totalTokensStaked.sub(_amount);
    } else {
      pool.totalTokensStaked = pool.totalTokensStaked.add(_amount);
    }
  }

  function _compareStr(string memory a, string memory b)
    private
    pure
    returns (bool)
  {
    return (keccak256(abi.encodePacked((a))) ==
      keccak256(abi.encodePacked((b))));
  }
}

// File: contracts/OKLGFaaS.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import './OKLGFaaSToken.sol';
import './OKLGProduct.sol';

/**
 * @title OKLGFaaS (sOKLG)
 * @author Lance Whatley
 * @notice Implements the master FaaS contract to keep track of all tokens being added
 * to be staked and staking.
 */
contract OKLGFaaS is OKLGProduct {
  // this is a mapping of tokenAddress => contractAddress[] that represents
  // a particular address for the token that someone has put up
  // to be staked and a list of contract addresses for the staking token
  // contracts paying out stakers for the given token.
  mapping(address => address[]) public tokensUpForStaking;
  address[] public allFarmingContracts;
  uint256 public totalStakingContracts;

  /**
   * @notice The constructor for the staking master contract.
   */
  constructor(address _tokenAddress, address _spendAddress)
    OKLGProduct(uint8(8), _tokenAddress, _spendAddress)
  {}

  function getAllFarmingContracts() external view returns (address[] memory) {
    return allFarmingContracts;
  }

  function getTokensForStaking(address _tokenAddress)
    external
    view
    returns (address[] memory)
  {
    return tokensUpForStaking[_tokenAddress];
  }

  function createNewTokenContract(
    address _rewardsTokenAddy,
    address _stakedTokenAddy,
    uint256 _supply,
    uint256 _perBlockAllocation,
    uint256 _lockedUntilDate,
    uint256 _timelockSeconds,
    bool _isStakedNft
  ) external payable {
    _payForService(0);

    // create new OKLGFaaSToken contract which will serve as the core place for
    // users to stake their tokens and earn rewards
    ERC20 _rewToken = ERC20(_rewardsTokenAddy);

    // Send the new contract all the tokens from the sending user to be staked and harvested
    _rewToken.transferFrom(msg.sender, address(this), _supply);

    // in order to handle tokens that take tax, are burned, etc. when transferring, need to get
    // the user's balance after transferring in order to send the remainder of the tokens
    // instead of the full original supply. Similar to slippage on a DEX
    uint256 _updatedSupply = _supply <= _rewToken.balanceOf(address(this))
      ? _supply
      : _rewToken.balanceOf(address(this));

    OKLGFaaSToken _contract = new OKLGFaaSToken(
      'Moontography Staking Token',
      'sOKLG',
      _updatedSupply,
      _rewardsTokenAddy,
      _stakedTokenAddy,
      msg.sender,
      _perBlockAllocation,
      _lockedUntilDate,
      _timelockSeconds,
      _isStakedNft
    );
    allFarmingContracts.push(address(_contract));
    tokensUpForStaking[_stakedTokenAddy].push(address(_contract));
    totalStakingContracts++;

    _rewToken.transfer(address(_contract), _updatedSupply);

    // do one more double check on balance of rewards token
    // in the staking contract and update if need be
    uint256 _finalSupply = _updatedSupply <=
      _rewToken.balanceOf(address(_contract))
      ? _updatedSupply
      : _rewToken.balanceOf(address(_contract));
    if (_updatedSupply != _finalSupply) {
      _contract.updateSupply(_finalSupply);
    }
  }

  function removeTokenContract(address _faasTokenAddy) external {
    OKLGFaaSToken _contract = OKLGFaaSToken(_faasTokenAddy);
    require(
      msg.sender == _contract.tokenOwner(),
      'user must be the original token owner to remove tokens'
    );
    require(
      block.timestamp > _contract.getLockedUntilDate() &&
        _contract.getLockedUntilDate() != 0,
      'it must be after the locked time the user originally configured and not locked forever'
    );

    _contract.removeStakeableTokens();
    totalStakingContracts--;
  }
}

// File: contracts/OKLGDividendDistributor.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';
import './interfaces/IConditional.sol';
import './interfaces/IMultiplier.sol';
import './OKLGWithdrawable.sol';

contract OKLGDividendDistributor is OKLGWithdrawable {
  using SafeMath for uint256;

  struct Dividend {
    uint256 totalExcluded; // excluded dividend
    uint256 totalRealised;
    uint256 lastClaim; // used for boosting logic
  }

  address public shareholderToken;
  uint256 public totalSharesDeposited; // will only be actual deposited tokens without handling any reflections or otherwise
  address wrappedNative;
  IUniswapV2Router02 router;

  // used to fetch in a frontend to get full list
  // of tokens that dividends can be claimed
  address[] public tokens;
  mapping(address => bool) tokenAwareness;

  mapping(address => uint256) shareholderClaims;

  // amount of shares a user has
  mapping(address => uint256) public shares;
  // dividend information per user
  mapping(address => mapping(address => Dividend)) public dividends;

  address public boostContract;
  address public boostMultiplierContract;
  uint256 public floorBoostClaimTimeSeconds = 60 * 60 * 12;
  uint256 public ceilBoostClaimTimeSeconds = 60 * 60 * 24;

  // per token dividends
  mapping(address => uint256) public totalDividends;
  mapping(address => uint256) public totalDistributed; // to be shown in UI
  mapping(address => uint256) public dividendsPerShare;

  uint256 public constant ACC_FACTOR = 10**36;

  constructor(
    address _dexRouter,
    address _shareholderToken,
    address _wrappedNative
  ) {
    router = IUniswapV2Router02(_dexRouter);
    shareholderToken = _shareholderToken;
    wrappedNative = _wrappedNative;
  }

  function stake(address token, uint256 amount) external {
    _stake(msg.sender, token, amount, false);
  }

  function _stake(
    address shareholder,
    address token,
    uint256 amount,
    bool contractOverride
  ) private {
    if (shares[shareholder] > 0) {
      distributeDividend(token, shareholder, false);
    }

    IERC20 shareContract = IERC20(shareholderToken);
    uint256 stakeAmount = amount == 0
      ? shareContract.balanceOf(shareholder)
      : amount;

    // for compounding we will pass in this contract override flag and assume the tokens
    // received by the contract during the compounding process are already here, therefore
    // whatever the amount is passed in is what we care about and leave it at that. If a normal
    // staking though by a user, transfer tokens from the user to the contract.
    uint256 finalAmount = amount;
    if (!contractOverride) {
      uint256 shareBalanceBefore = shareContract.balanceOf(address(this));
      shareContract.transferFrom(shareholder, address(this), stakeAmount);
      finalAmount = shareContract.balanceOf(address(this)).sub(
        shareBalanceBefore
      );
    }

    totalSharesDeposited = totalSharesDeposited.add(finalAmount);
    shares[shareholder] += finalAmount;
    dividends[shareholder][token].totalExcluded = getCumulativeDividends(
      token,
      shares[shareholder]
    );
  }

  function unstake(address token, uint256 amount) external {
    require(
      shares[msg.sender] > 0 && (amount == 0 || shares[msg.sender] <= amount),
      'you can only unstake if you have some staked'
    );
    distributeDividend(token, msg.sender, false);

    IERC20 shareContract = IERC20(shareholderToken);

    // need to handle reflections tokens
    uint256 baseWithdrawAmount = amount == 0 ? shares[msg.sender] : amount;
    uint256 totalSharesBalance = shareContract.balanceOf(address(this)).sub(
      totalDividends[shareholderToken].sub(totalDistributed[shareholderToken])
    );
    uint256 appreciationRatio = totalSharesBalance.div(totalSharesDeposited);
    uint256 finalWithdrawAmount = baseWithdrawAmount.mul(appreciationRatio);

    shareContract.transfer(msg.sender, finalWithdrawAmount);
    totalSharesDeposited = totalSharesDeposited.sub(baseWithdrawAmount);
    shares[msg.sender] -= baseWithdrawAmount;
    dividends[msg.sender][token].totalExcluded = getCumulativeDividends(
      token,
      shares[msg.sender]
    );
  }

  // tokenAddress == address(0) means native token
  // any other token should be ERC20 listed on DEX router provided in constructor
  //
  // NOTE: Using this function will add tokens to the core rewards/dividends to be
  // distributed to all shareholders. However, to implement boosting, the token
  // should be directly transferred to this contract. Anything above and
  // beyond the totalDividends[tokenAddress] amount will be used for boosting.
  function depositDividends(address tokenAddress, uint256 erc20DirectAmount)
    external
    payable
  {
    require(
      erc20DirectAmount > 0 || msg.value > 0,
      'value must be greater than 0'
    );
    require(
      totalSharesDeposited > 0,
      'must be shares deposited to be rewarded dividends'
    );

    if (!tokenAwareness[tokenAddress]) {
      tokenAwareness[tokenAddress] = true;
      tokens.push(tokenAddress);
    }

    IERC20 token;
    uint256 amount;
    if (tokenAddress == address(0)) {
      payable(address(this)).call{ value: msg.value }('');
      amount = msg.value;
    } else if (erc20DirectAmount > 0) {
      token = IERC20(tokenAddress);
      uint256 balanceBefore = token.balanceOf(address(this));

      token.transferFrom(msg.sender, address(this), erc20DirectAmount);

      amount = token.balanceOf(address(this)).sub(balanceBefore);
    } else {
      token = IERC20(tokenAddress);
      uint256 balanceBefore = token.balanceOf(address(this));

      address[] memory path = new address[](2);
      path[0] = wrappedNative;
      path[1] = tokenAddress;

      router.swapExactETHForTokensSupportingFeeOnTransferTokens{
        value: msg.value
      }(0, path, address(this), block.timestamp);

      amount = token.balanceOf(address(this)).sub(balanceBefore);
    }

    totalDividends[tokenAddress] = totalDividends[tokenAddress].add(amount);
    dividendsPerShare[tokenAddress] = dividendsPerShare[tokenAddress].add(
      ACC_FACTOR.mul(amount).div(totalSharesDeposited)
    );
  }

  function distributeDividend(
    address token,
    address shareholder,
    bool compound
  ) internal {
    if (shares[shareholder] == 0) {
      return;
    }

    uint256 amount = getUnpaidEarnings(token, shareholder);
    if (amount > 0) {
      totalDistributed[token] = totalDistributed[token].add(amount);
      // native transfer
      if (token == address(0)) {
        if (compound) {
          IERC20 shareToken = IERC20(shareholderToken);
          uint256 balBefore = shareToken.balanceOf(address(this));
          address[] memory path = new address[](2);
          path[0] = wrappedNative;
          path[1] = shareholderToken;
          router.swapExactETHForTokensSupportingFeeOnTransferTokens{
            value: amount
          }(0, path, address(this), block.timestamp);
          uint256 amountReceived = shareToken.balanceOf(address(this)).sub(
            balBefore
          );
          _stake(shareholder, token, amountReceived, true);
        } else {
          payable(shareholder).call{ value: amount }('');
        }
      } else {
        IERC20(token).transfer(shareholder, amount);
      }

      _payBoostedRewards(token, shareholder);

      shareholderClaims[shareholder] = block.timestamp;
      dividends[shareholder][token].totalRealised = dividends[shareholder][
        token
      ].totalRealised.add(amount);
      dividends[shareholder][token].totalExcluded = getCumulativeDividends(
        token,
        shares[shareholder]
      );
      dividends[shareholder][token].lastClaim = block.timestamp;
    }
  }

  function _payBoostedRewards(address token, address shareholder) internal {
    bool canCurrentlyClaim = canClaimBoostedRewards(token, shareholder) &&
      eligibleForRewardBooster(shareholder);
    if (!canCurrentlyClaim) {
      return;
    }

    uint256 amount = calculateBoostRewards(token, shareholder);
    if (amount > 0) {
      if (token == address(0)) {
        payable(shareholder).call{ value: amount }('');
      } else {
        IERC20(token).transfer(shareholder, amount);
      }
    }
  }

  function claimDividend(address token, bool compound) external {
    distributeDividend(token, msg.sender, compound);
  }

  function canClaimBoostedRewards(address token, address shareholder)
    public
    view
    returns (bool)
  {
    return
      dividends[shareholder][token].lastClaim == 0 ||
      (block.timestamp >
        dividends[shareholder][token].lastClaim.add(
          floorBoostClaimTimeSeconds
        ) &&
        block.timestamp <=
        dividends[shareholder][token].lastClaim.add(ceilBoostClaimTimeSeconds));
  }

  function getDividendTokens() external view returns (address[] memory) {
    return tokens;
  }

  function getBoostMultiplier(address wallet) public view returns (uint256) {
    return
      boostMultiplierContract == address(0)
        ? 0
        : IMultiplier(boostMultiplierContract).getMultiplier(wallet);
  }

  function calculateBoostRewards(address token, address shareholder)
    public
    view
    returns (uint256)
  {
    if (totalSharesDeposited == 0) {
      return 0;
    }

    uint256 availableBoostRewards = address(this).balance;
    if (token != address(0)) {
      availableBoostRewards = IERC20(token).balanceOf(address(this));
    }
    uint256 excludedRewards = totalDividends[token].sub(
      totalDistributed[token]
    );
    IERC20 shareToken = IERC20(shareholderToken);
    availableBoostRewards = (availableBoostRewards.sub(excludedRewards))
      .mul(shares[shareholder])
      .div(totalSharesDeposited);

    uint256 rewardsWithBooster = eligibleForRewardBooster(shareholder)
      ? availableBoostRewards.add(
        availableBoostRewards.mul(getBoostMultiplier(shareholder)).div(10**2)
      )
      : availableBoostRewards;
    return
      rewardsWithBooster > availableBoostRewards
        ? availableBoostRewards
        : rewardsWithBooster;
  }

  function eligibleForRewardBooster(address shareholder)
    public
    view
    returns (bool)
  {
    return
      boostContract != address(0) &&
      IConditional(boostContract).passesTest(shareholder);
  }

  // returns the unpaid earnings
  function getUnpaidEarnings(address token, address shareholder)
    public
    view
    returns (uint256)
  {
    if (shares[shareholder] == 0) {
      return 0;
    }

    uint256 earnedDividends = getCumulativeDividends(
      token,
      shares[shareholder]
    );
    uint256 dividendsExcluded = dividends[shareholder][token].totalExcluded;
    if (earnedDividends <= dividendsExcluded) {
      return 0;
    }

    return earnedDividends.sub(dividendsExcluded);
  }

  function getCumulativeDividends(address token, uint256 share)
    internal
    view
    returns (uint256)
  {
    return share.mul(dividendsPerShare[token]).div(ACC_FACTOR);
  }

  function setShareholderToken(address _token) external onlyOwner {
    shareholderToken = _token;
  }

  function setBoostClaimTimeInfo(uint256 floor, uint256 ceil)
    external
    onlyOwner
  {
    floorBoostClaimTimeSeconds = floor;
    ceilBoostClaimTimeSeconds = ceil;
  }

  function setBoostContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IConditional _contCheck = IConditional(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.passesTest(address(0)) == true ||
          _contCheck.passesTest(address(0)) == false,
        'contract does not implement interface'
      );
    }
    boostContract = _contract;
  }

  function setBoostMultiplierContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IMultiplier _contCheck = IMultiplier(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.getMultiplier(address(0)) >= 0,
        'contract does not implement interface'
      );
    }
    boostMultiplierContract = _contract;
  }

  receive() external payable {}
}

// File: contracts/OKLG.sol



/*

ok.let's.go. ($OKLG)

Website = https://oklg.io
Telegram = https://t.me/ok_lg
Twitter = https://twitter.com/oklgio

*/

// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Address.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';
import './interfaces/IConditional.sol';

contract OKLG is Context, IERC20, Ownable {
  using SafeMath for uint256;
  using Address for address;

  address payable public treasuryWallet =
    payable(0xDb3AC91239b79Fae75c21E1f75a189b1D75dD906);
  address public constant deadAddress =
    0x000000000000000000000000000000000000dEaD;

  mapping(address => uint256) private _rOwned;
  mapping(address => uint256) private _tOwned;
  mapping(address => mapping(address => uint256)) private _allowances;
  mapping(address => bool) private _isSniper;
  address[] private _confirmedSnipers;

  uint256 public rewardsClaimTimeSeconds = 60 * 60 * 4; // 4 hours
  mapping(address => uint256) private _rewardsLastClaim;

  mapping(address => bool) private _isExcludedFee;
  mapping(address => bool) private _isExcludedReward;
  address[] private _excluded;

  string private constant _name = 'ok.lets.go.';
  string private constant _symbol = 'OKLG';
  uint8 private constant _decimals = 9;

  uint256 private constant MAX = ~uint256(0);
  uint256 private constant _tTotal = 420690000000 * 10**_decimals;
  uint256 private _rTotal = (MAX - (MAX % _tTotal));
  uint256 private _tFeeTotal;

  uint256 public reflectionFee = 2;
  uint256 private _previousReflectFee = reflectionFee;

  uint256 public treasuryFee = 4;
  uint256 private _previousTreasuryFee = treasuryFee;

  uint256 public ethRewardsFee = 2;
  uint256 private _previousETHRewardsFee = ethRewardsFee;
  uint256 public ethRewardsBalance;

  uint256 public buybackFee = 2;
  uint256 private _previousBuybackFee = buybackFee;
  address public buybackTokenAddress = address(this);
  address public buybackReceiver = deadAddress;

  uint256 public feeSellMultiplier = 1;
  uint256 public feeRate = 2;
  uint256 public launchTime;

  uint256 public boostRewardsPercent = 50;

  address public boostRewardsContract;
  address public feeExclusionContract;

  IUniswapV2Router02 public uniswapV2Router;
  address public uniswapV2Pair;
  mapping(address => bool) private _isUniswapPair;

  // PancakeSwap: 0x10ED43C718714eb63d5aA57B78B54704E256024E
  // Uniswap V2: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
  address private constant _uniswapRouterAddress =
    0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;

  bool private _inSwapAndLiquify;
  bool private _isSelling;
  bool private _tradingOpen = false;
  bool private _transferOpen = false;

  event SendETHRewards(address to, uint256 amountETH);
  event SendTokenRewards(address to, address token, uint256 amount);
  event SwapETHForTokens(address whereTo, uint256 amountIn, address[] path);
  event SwapTokensForETH(uint256 amountIn, address[] path);
  event SwapAndLiquify(
    uint256 tokensSwappedForEth,
    uint256 ethAddedForLp,
    uint256 tokensAddedForLp
  );

  modifier lockTheSwap() {
    _inSwapAndLiquify = true;
    _;
    _inSwapAndLiquify = false;
  }

  constructor() {
    _rOwned[_msgSender()] = _rTotal;
    emit Transfer(address(0), _msgSender(), _tTotal);
  }

  function initContract() external onlyOwner {
    IUniswapV2Router02 _uniswapV2Router = IUniswapV2Router02(
      _uniswapRouterAddress
    );
    uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory()).createPair(
      address(this),
      _uniswapV2Router.WETH()
    );

    uniswapV2Router = _uniswapV2Router;

    _isExcludedFee[owner()] = true;
    _isExcludedFee[address(this)] = true;
  }

  function openTrading() external onlyOwner {
    treasuryFee = _previousTreasuryFee;
    ethRewardsFee = _previousETHRewardsFee;
    reflectionFee = _previousReflectFee;
    buybackFee = _previousBuybackFee;
    _tradingOpen = true;
    _transferOpen = true;
    launchTime = block.timestamp;
  }

  function name() external pure returns (string memory) {
    return _name;
  }

  function symbol() external pure returns (string memory) {
    return _symbol;
  }

  function decimals() external pure returns (uint8) {
    return _decimals;
  }

  function totalSupply() external pure override returns (uint256) {
    return _tTotal;
  }

  function balanceOf(address account) public view override returns (uint256) {
    if (_isExcludedReward[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
  }

  function transfer(address recipient, uint256 amount)
    external
    override
    returns (bool)
  {
    _transfer(_msgSender(), recipient, amount);
    return true;
  }

  function allowance(address owner, address spender)
    external
    view
    override
    returns (uint256)
  {
    return _allowances[owner][spender];
  }

  function approve(address spender, uint256 amount)
    external
    override
    returns (bool)
  {
    _approve(_msgSender(), spender, amount);
    return true;
  }

  function transferFrom(
    address sender,
    address recipient,
    uint256 amount
  ) external override returns (bool) {
    _transfer(sender, recipient, amount);
    _approve(
      sender,
      _msgSender(),
      _allowances[sender][_msgSender()].sub(
        amount,
        'ERC20: transfer amount exceeds allowance'
      )
    );
    return true;
  }

  function increaseAllowance(address spender, uint256 addedValue)
    external
    virtual
    returns (bool)
  {
    _approve(
      _msgSender(),
      spender,
      _allowances[_msgSender()][spender].add(addedValue)
    );
    return true;
  }

  function decreaseAllowance(address spender, uint256 subtractedValue)
    external
    virtual
    returns (bool)
  {
    _approve(
      _msgSender(),
      spender,
      _allowances[_msgSender()][spender].sub(
        subtractedValue,
        'ERC20: decreased allowance below zero'
      )
    );
    return true;
  }

  function getLastETHRewardsClaim(address wallet)
    external
    view
    returns (uint256)
  {
    return _rewardsLastClaim[wallet];
  }

  function totalFees() external view returns (uint256) {
    return _tFeeTotal;
  }

  function deliver(uint256 tAmount) external {
    address sender = _msgSender();
    require(
      !_isExcludedReward[sender],
      'Excluded addresses cannot call this function'
    );
    (uint256 rAmount, , , , , ) = _getValues(sender, tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rTotal = _rTotal.sub(rAmount);
    _tFeeTotal = _tFeeTotal.add(tAmount);
  }

  function reflectionFromToken(uint256 tAmount, bool deductTransferFee)
    external
    view
    returns (uint256)
  {
    require(tAmount <= _tTotal, 'Amount must be less than supply');
    if (!deductTransferFee) {
      (uint256 rAmount, , , , , ) = _getValues(address(0), tAmount);
      return rAmount;
    } else {
      (, uint256 rTransferAmount, , , , ) = _getValues(address(0), tAmount);
      return rTransferAmount;
    }
  }

  function tokenFromReflection(uint256 rAmount) public view returns (uint256) {
    require(rAmount <= _rTotal, 'Amount must be less than total reflections');
    uint256 currentRate = _getRate();
    return rAmount.div(currentRate);
  }

  function excludeFromReward(address account) external onlyOwner {
    require(!_isExcludedReward[account], 'Account is already excluded');
    if (_rOwned[account] > 0) {
      _tOwned[account] = tokenFromReflection(_rOwned[account]);
    }
    _isExcludedReward[account] = true;
    _excluded.push(account);
  }

  function includeInReward(address account) external onlyOwner {
    require(_isExcludedReward[account], 'Account is already included');
    for (uint256 i = 0; i < _excluded.length; i++) {
      if (_excluded[i] == account) {
        _excluded[i] = _excluded[_excluded.length - 1];
        _tOwned[account] = 0;
        _isExcludedReward[account] = false;
        _excluded.pop();
        break;
      }
    }
  }

  function _approve(
    address owner,
    address spender,
    uint256 amount
  ) private {
    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);
  }

  function _transfer(
    address from,
    address to,
    uint256 amount
  ) private {
    require(from != address(0), 'ERC20: transfer from the zero address');
    require(to != address(0), 'ERC20: transfer to the zero address');
    require(amount > 0, 'Transfer amount must be greater than zero');
    require(!_isSniper[to], 'Stop sniping!');
    require(!_isSniper[from], 'Stop sniping!');
    require(!_isSniper[_msgSender()], 'Stop sniping!');
    require(
      _transferOpen || from == owner(),
      'transferring tokens is not currently allowed'
    );

    // reset receiver's timer to prevent users buying and
    // immmediately transferring to buypass timer
    _rewardsLastClaim[to] = block.timestamp;

    bool excludedFromFee = false;

    // buy
    if (
      (from == uniswapV2Pair || _isUniswapPair[from]) &&
      to != address(uniswapV2Router)
    ) {
      // normal buy, check for snipers
      if (!isExcludedFromFee(to)) {
        require(_tradingOpen, 'Trading not yet enabled.');

        // antibot
        if (block.timestamp == launchTime) {
          _isSniper[to] = true;
          _confirmedSnipers.push(to);
        }
        _rewardsLastClaim[from] = block.timestamp;
      } else {
        // set excluded flag for takeFee below since buyer is excluded
        excludedFromFee = true;
      }
    }

    // sell
    if (
      !_inSwapAndLiquify &&
      _tradingOpen &&
      (to == uniswapV2Pair || _isUniswapPair[to])
    ) {
      uint256 _contractTokenBalance = balanceOf(address(this));
      if (_contractTokenBalance > 0) {
        if (
          _contractTokenBalance > balanceOf(uniswapV2Pair).mul(feeRate).div(100)
        ) {
          _contractTokenBalance = balanceOf(uniswapV2Pair).mul(feeRate).div(
            100
          );
        }
        _swapTokens(_contractTokenBalance);
      }
      _rewardsLastClaim[from] = block.timestamp;
      _isSelling = true;
      excludedFromFee = isExcludedFromFee(from);
    }

    bool takeFee = false;

    // take fee only on swaps
    if (
      (from == uniswapV2Pair ||
        to == uniswapV2Pair ||
        _isUniswapPair[to] ||
        _isUniswapPair[from]) && !excludedFromFee
    ) {
      takeFee = true;
    }

    _tokenTransfer(from, to, amount, takeFee);
    _isSelling = false;
  }

  function _swapTokens(uint256 _contractTokenBalance) private lockTheSwap {
    uint256 ethBalanceBefore = address(this).balance;
    _swapTokensForEth(_contractTokenBalance);
    uint256 ethBalanceAfter = address(this).balance;
    uint256 ethBalanceUpdate = ethBalanceAfter.sub(ethBalanceBefore);
    uint256 _liquidityFeeTotal = _liquidityFeeAggregate(address(0));

    ethRewardsBalance += ethBalanceUpdate.mul(ethRewardsFee).div(
      _liquidityFeeTotal
    );

    // send ETH to treasury address
    uint256 treasuryETHBalance = ethBalanceUpdate.mul(treasuryFee).div(
      _liquidityFeeTotal
    );
    if (treasuryETHBalance > 0) {
      _sendETHToTreasury(treasuryETHBalance);
    }

    // buy back
    uint256 buybackETHBalance = ethBalanceUpdate.mul(buybackFee).div(
      _liquidityFeeTotal
    );
    if (buybackETHBalance > 0) {
      _buyBackTokens(buybackETHBalance);
    }
  }

  function _sendETHToTreasury(uint256 amount) private {
    treasuryWallet.call{ value: amount }('');
  }

  function _buyBackTokens(uint256 amount) private {
    // generate the uniswap pair path of token -> weth
    address[] memory path = new address[](2);
    path[0] = uniswapV2Router.WETH();
    path[1] = buybackTokenAddress;

    // make the swap
    uniswapV2Router.swapExactETHForTokensSupportingFeeOnTransferTokens{
      value: amount
    }(
      0, // accept any amount of tokens
      path,
      buybackReceiver,
      block.timestamp
    );

    emit SwapETHForTokens(buybackReceiver, amount, path);
  }

  function _swapTokensForEth(uint256 tokenAmount) private {
    // generate the uniswap pair path of token -> weth
    address[] memory path = new address[](2);
    path[0] = address(this);
    path[1] = uniswapV2Router.WETH();

    _approve(address(this), address(uniswapV2Router), tokenAmount);

    // make the swap
    uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
      tokenAmount,
      0, // accept any amount of ETH
      path,
      address(this), // the contract
      block.timestamp
    );

    emit SwapTokensForETH(tokenAmount, path);
  }

  function _tokenTransfer(
    address sender,
    address recipient,
    uint256 amount,
    bool takeFee
  ) private {
    if (!takeFee) _removeAllFee();

    if (_isExcludedReward[sender] && !_isExcludedReward[recipient]) {
      _transferFromExcluded(sender, recipient, amount);
    } else if (!_isExcludedReward[sender] && _isExcludedReward[recipient]) {
      _transferToExcluded(sender, recipient, amount);
    } else if (_isExcludedReward[sender] && _isExcludedReward[recipient]) {
      _transferBothExcluded(sender, recipient, amount);
    } else {
      _transferStandard(sender, recipient, amount);
    }

    if (!takeFee) _restoreAllFee();
  }

  function _transferStandard(
    address sender,
    address recipient,
    uint256 tAmount
  ) private {
    (
      uint256 rAmount,
      uint256 rTransferAmount,
      uint256 rFee,
      uint256 tTransferAmount,
      uint256 tFee,
      uint256 tLiquidity
    ) = _getValues(sender, tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
    _takeLiquidity(tLiquidity);
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
  }

  function _transferToExcluded(
    address sender,
    address recipient,
    uint256 tAmount
  ) private {
    (
      uint256 rAmount,
      uint256 rTransferAmount,
      uint256 rFee,
      uint256 tTransferAmount,
      uint256 tFee,
      uint256 tLiquidity
    ) = _getValues(sender, tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _tOwned[recipient] = _tOwned[recipient].add(tTransferAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
    _takeLiquidity(tLiquidity);
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
  }

  function _transferFromExcluded(
    address sender,
    address recipient,
    uint256 tAmount
  ) private {
    (
      uint256 rAmount,
      uint256 rTransferAmount,
      uint256 rFee,
      uint256 tTransferAmount,
      uint256 tFee,
      uint256 tLiquidity
    ) = _getValues(sender, tAmount);
    _tOwned[sender] = _tOwned[sender].sub(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
    _takeLiquidity(tLiquidity);
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
  }

  function _transferBothExcluded(
    address sender,
    address recipient,
    uint256 tAmount
  ) private {
    (
      uint256 rAmount,
      uint256 rTransferAmount,
      uint256 rFee,
      uint256 tTransferAmount,
      uint256 tFee,
      uint256 tLiquidity
    ) = _getValues(sender, tAmount);
    _tOwned[sender] = _tOwned[sender].sub(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _tOwned[recipient] = _tOwned[recipient].add(tTransferAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
    _takeLiquidity(tLiquidity);
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
  }

  function _reflectFee(uint256 rFee, uint256 tFee) private {
    _rTotal = _rTotal.sub(rFee);
    _tFeeTotal = _tFeeTotal.add(tFee);
  }

  function _getValues(address seller, uint256 tAmount)
    private
    view
    returns (
      uint256,
      uint256,
      uint256,
      uint256,
      uint256,
      uint256
    )
  {
    (uint256 tTransferAmount, uint256 tFee, uint256 tLiquidity) = _getTValues(
      seller,
      tAmount
    );
    (uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(
      tAmount,
      tFee,
      tLiquidity,
      _getRate()
    );
    return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee, tLiquidity);
  }

  function _getTValues(address seller, uint256 tAmount)
    private
    view
    returns (
      uint256,
      uint256,
      uint256
    )
  {
    uint256 tFee = _calculateReflectFee(tAmount);
    uint256 tLiquidity = _calculateLiquidityFee(seller, tAmount);
    uint256 tTransferAmount = tAmount.sub(tFee).sub(tLiquidity);
    return (tTransferAmount, tFee, tLiquidity);
  }

  function _getRValues(
    uint256 tAmount,
    uint256 tFee,
    uint256 tLiquidity,
    uint256 currentRate
  )
    private
    pure
    returns (
      uint256,
      uint256,
      uint256
    )
  {
    uint256 rAmount = tAmount.mul(currentRate);
    uint256 rFee = tFee.mul(currentRate);
    uint256 rLiquidity = tLiquidity.mul(currentRate);
    uint256 rTransferAmount = rAmount.sub(rFee).sub(rLiquidity);
    return (rAmount, rTransferAmount, rFee);
  }

  function _getRate() private view returns (uint256) {
    (uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
    return rSupply.div(tSupply);
  }

  function _getCurrentSupply() private view returns (uint256, uint256) {
    uint256 rSupply = _rTotal;
    uint256 tSupply = _tTotal;
    for (uint256 i = 0; i < _excluded.length; i++) {
      if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply)
        return (_rTotal, _tTotal);
      rSupply = rSupply.sub(_rOwned[_excluded[i]]);
      tSupply = tSupply.sub(_tOwned[_excluded[i]]);
    }
    if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
    return (rSupply, tSupply);
  }

  function _takeLiquidity(uint256 tLiquidity) private {
    uint256 currentRate = _getRate();
    uint256 rLiquidity = tLiquidity.mul(currentRate);
    _rOwned[address(this)] = _rOwned[address(this)].add(rLiquidity);
    if (_isExcludedReward[address(this)])
      _tOwned[address(this)] = _tOwned[address(this)].add(tLiquidity);
  }

  function _calculateReflectFee(uint256 _amount)
    private
    view
    returns (uint256)
  {
    return _amount.mul(reflectionFee).div(10**2);
  }

  function _liquidityFeeAggregate(address seller)
    private
    view
    returns (uint256)
  {
    uint256 feeMultiplier = _isSelling && !canClaimRewards(seller)
      ? feeSellMultiplier
      : 1;
    return (treasuryFee.add(ethRewardsFee).add(buybackFee)).mul(feeMultiplier);
  }

  function _calculateLiquidityFee(address seller, uint256 _amount)
    private
    view
    returns (uint256)
  {
    return _amount.mul(_liquidityFeeAggregate(seller)).div(10**2);
  }

  function _removeAllFee() private {
    if (
      reflectionFee == 0 &&
      treasuryFee == 0 &&
      ethRewardsFee == 0 &&
      buybackFee == 0
    ) return;

    _previousReflectFee = reflectionFee;
    _previousTreasuryFee = treasuryFee;
    _previousETHRewardsFee = ethRewardsFee;
    _previousBuybackFee = buybackFee;

    reflectionFee = 0;
    treasuryFee = 0;
    ethRewardsFee = 0;
    buybackFee = 0;
  }

  function _restoreAllFee() private {
    reflectionFee = _previousReflectFee;
    treasuryFee = _previousTreasuryFee;
    ethRewardsFee = _previousETHRewardsFee;
    buybackFee = _previousBuybackFee;
  }

  function getSellSlippage(address seller) external view returns (uint256) {
    uint256 feeAgg = reflectionFee.add(treasuryFee).add(ethRewardsFee).add(
      buybackFee
    );
    return
      isExcludedFromFee(seller) ? 0 : !canClaimRewards(seller)
        ? feeAgg.mul(feeSellMultiplier)
        : feeAgg;
  }

  function isUniswapPair(address _pair) external view returns (bool) {
    if (_pair == uniswapV2Pair) return true;
    return _isUniswapPair[_pair];
  }

  function eligibleForRewardBooster(address wallet) public view returns (bool) {
    return
      boostRewardsContract != address(0) &&
      IConditional(boostRewardsContract).passesTest(wallet);
  }

  function isExcludedFromFee(address account) public view returns (bool) {
    return
      _isExcludedFee[account] ||
      (feeExclusionContract != address(0) &&
        IConditional(feeExclusionContract).passesTest(account));
  }

  function isExcludedFromReward(address account) external view returns (bool) {
    return _isExcludedReward[account];
  }

  function excludeFromFee(address account) external onlyOwner {
    _isExcludedFee[account] = true;
  }

  function includeInFee(address account) external onlyOwner {
    _isExcludedFee[account] = false;
  }

  function setRewardsClaimTimeSeconds(uint256 _seconds) external onlyOwner {
    rewardsClaimTimeSeconds = _seconds;
  }

  function setReflectionFeePercent(uint256 _newFee) external onlyOwner {
    require(_newFee <= 10, 'fee cannot exceed 10%');
    reflectionFee = _newFee;
  }

  function setTreasuryFeePercent(uint256 _newFee) external onlyOwner {
    require(_newFee <= 10, 'fee cannot exceed 10%');
    treasuryFee = _newFee;
  }

  function setETHRewardsFeeFeePercent(uint256 _newFee) external onlyOwner {
    require(_newFee <= 10, 'fee cannot exceed 10%');
    ethRewardsFee = _newFee;
  }

  function setBuybackFeePercent(uint256 _newFee) external onlyOwner {
    require(_newFee <= 10, 'fee cannot exceed 10%');
    buybackFee = _newFee;
  }

  function setFeeSellMultiplier(uint256 multiplier) external onlyOwner {
    require(
      multiplier > 0 && multiplier <= 5,
      'must be greater than 0 and less than or equal to 5'
    );
    require(
      _liquidityFeeAggregate(address(0)).mul(multiplier) < 40,
      'multiplier should not make total fee more than 40%'
    );
    feeSellMultiplier = multiplier;
  }

  function setTreasuryAddress(address _treasuryWallet) external onlyOwner {
    treasuryWallet = payable(_treasuryWallet);
  }

  function setBuybackTokenAddress(address _tokenAddress) external onlyOwner {
    buybackTokenAddress = _tokenAddress;
  }

  function setBuybackReceiver(address _receiver) external onlyOwner {
    buybackReceiver = _receiver;
  }

  function addUniswapPair(address _pair) external onlyOwner {
    _isUniswapPair[_pair] = true;
  }

  function removeUniswapPair(address _pair) external onlyOwner {
    _isUniswapPair[_pair] = false;
  }

  function setCanTransfer(bool _canTransfer) external onlyOwner {
    _transferOpen = _canTransfer;
  }

  function setBoostRewardsPercent(uint256 perc) external onlyOwner {
    boostRewardsPercent = perc;
  }

  function setBoostRewardsContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IConditional _contCheck = IConditional(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.passesTest(address(0)) == true ||
          _contCheck.passesTest(address(0)) == false,
        'contract does not implement interface'
      );
    }
    boostRewardsContract = _contract;
  }

  function setFeeExclusionContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IConditional _contCheck = IConditional(_contract);
      // allow setting to zero address to effectively turn off check logic
      require(
        _contCheck.passesTest(address(0)) == true ||
          _contCheck.passesTest(address(0)) == false,
        'contract does not implement interface'
      );
    }
    feeExclusionContract = _contract;
  }

  function isRemovedSniper(address account) external view returns (bool) {
    return _isSniper[account];
  }

  function removeSniper(address account) external onlyOwner {
    require(account != _uniswapRouterAddress, 'We can not blacklist Uniswap');
    require(!_isSniper[account], 'Account is already blacklisted');
    _isSniper[account] = true;
    _confirmedSnipers.push(account);
  }

  function amnestySniper(address account) external onlyOwner {
    require(_isSniper[account], 'Account is not blacklisted');
    for (uint256 i = 0; i < _confirmedSnipers.length; i++) {
      if (_confirmedSnipers[i] == account) {
        _confirmedSnipers[i] = _confirmedSnipers[_confirmedSnipers.length - 1];
        _isSniper[account] = false;
        _confirmedSnipers.pop();
        break;
      }
    }
  }

  function calculateETHRewards(address wallet) public view returns (uint256) {
    uint256 baseRewards = ethRewardsBalance.mul(balanceOf(wallet)).div(
      _tTotal.sub(balanceOf(deadAddress)) // circulating supply
    );
    uint256 rewardsWithBooster = eligibleForRewardBooster(wallet)
      ? baseRewards.add(baseRewards.mul(boostRewardsPercent).div(10**2))
      : baseRewards;
    return
      rewardsWithBooster > ethRewardsBalance ? baseRewards : rewardsWithBooster;
  }

  function calculateTokenRewards(address wallet, address tokenAddress)
    public
    view
    returns (uint256)
  {
    IERC20 token = IERC20(tokenAddress);
    uint256 contractTokenBalance = token.balanceOf(address(this));
    uint256 baseRewards = contractTokenBalance.mul(balanceOf(wallet)).div(
      _tTotal.sub(balanceOf(deadAddress)) // circulating supply
    );
    uint256 rewardsWithBooster = eligibleForRewardBooster(wallet)
      ? baseRewards.add(baseRewards.mul(boostRewardsPercent).div(10**2))
      : baseRewards;
    return
      rewardsWithBooster > contractTokenBalance
        ? baseRewards
        : rewardsWithBooster;
  }

  function claimETHRewards() external {
    require(
      balanceOf(_msgSender()) > 0,
      'You must have a balance to claim ETH rewards'
    );
    require(
      canClaimRewards(_msgSender()),
      'Must wait claim period before claiming rewards'
    );
    _rewardsLastClaim[_msgSender()] = block.timestamp;

    uint256 rewardsSent = calculateETHRewards(_msgSender());
    ethRewardsBalance -= rewardsSent;
    _msgSender().call{ value: rewardsSent }('');
    emit SendETHRewards(_msgSender(), rewardsSent);
  }

  function canClaimRewards(address user) public view returns (bool) {
    return
      block.timestamp > _rewardsLastClaim[user].add(rewardsClaimTimeSeconds);
  }

  function claimTokenRewards(address token) external {
    require(
      balanceOf(_msgSender()) > 0,
      'You must have a balance to claim rewards'
    );
    require(
      IERC20(token).balanceOf(address(this)) > 0,
      'We must have a token balance to claim rewards'
    );
    require(
      canClaimRewards(_msgSender()),
      'Must wait claim period before claiming rewards'
    );
    _rewardsLastClaim[_msgSender()] = block.timestamp;

    uint256 rewardsSent = calculateTokenRewards(_msgSender(), token);
    IERC20(token).transfer(_msgSender(), rewardsSent);
    emit SendTokenRewards(_msgSender(), token, rewardsSent);
  }

  function setFeeRate(uint256 _rate) external onlyOwner {
    feeRate = _rate;
  }

  function emergencyWithdraw() external onlyOwner {
    payable(owner()).call{ value: address(this).balance }('');
  }

  // to recieve ETH from uniswapV2Router when swaping
  receive() external payable {}
}

// File: contracts/unaudited/UnicryptUnlockOverride.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '../interfaces/IUnlockCondition.sol';

contract UnicryptUnlockOverride is IUnlockCondition, Ownable {
  bool public isUnlocked;

  function unlockTokens() external view override returns (bool) {
    return isUnlocked;
  }

  function setIsUnlocked(bool _isUnlocked) external onlyOwner {
    isUnlocked = _isUnlocked;
  }
}

// File: contracts/interfaces/IUnlockCondition.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

/**
 * @dev Interface used by Unicrypt token locker/vester for premature unlock condition
 */
interface IUnlockCondition {
  function unlockTokens() external view returns (bool);
}

// File: contracts/OKLetsApe.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol';
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol';
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
import './interfaces/IERC721Helpers.sol';
import './utils/Counters.sol';

/**
 *
 * ok.lets.ape. NFT contract
 *
 */
contract OKLetsApe is
  Ownable,
  ERC721Burnable,
  ERC721Enumerable,
  ERC721Pausable
{
  using SafeMath for uint256;
  using Strings for uint256;
  using Counters for Counters.Counter;

  // Token id counter
  Counters.Counter private _tokenIds;

  // Sale round counters
  Counters.Counter public _preSaleRound;
  Counters.Counter public _publicSaleRound;

  // Mints per sale round counter
  Counters.Counter public _tokensMintedPerSaleRound;

  // Base token uri
  string private baseTokenURI; // baseTokenURI can point to IPFS folder like https://ipfs.io/ipfs/{cid}/

  // Payment address
  address private paymentAddress;

  // Royalties address
  address private royaltyAddress;

  // Royalties basis points (percentage using 2 decimals - 10000 = 100, 0 = 0)
  uint256 private royaltyBasisPoints = 1000; // 10%

  // Token info
  string public constant TOKEN_NAME = 'ok.lets.ape.';
  string public constant TOKEN_SYMBOL = 'OKLApe';
  uint256 public constant TOTAL_TOKENS = 10000;

  // Mint cost and max per wallet
  uint256 public mintCost = 0.0542069 ether;

  // Mint cost contract
  address public mintCostContract;

  // Max wallet amount
  uint256 public maxWalletAmount = 10;

  // Amount of tokens to mint before automatically stopping public sale
  uint256 public maxMintsPerSaleRound = 1000;

  // Pre sale/Public sale active
  bool public preSaleActive;
  bool public publicSaleActive;

  // Presale whitelist
  mapping(address => bool) public presaleWhitelist;

  // Authorized addresses
  mapping(address => bool) public authorizedAddresses;

  //-- Events --//
  event RoyaltyBasisPoints(uint256 indexed _royaltyBasisPoints);

  //-- Modifiers --//

  // Public sale active modifier
  modifier whenPreSaleActive() {
    require(preSaleActive, 'Pre sale is not active');
    _;
  }

  // Public sale active modifier
  modifier whenPublicSaleActive() {
    require(publicSaleActive, 'Public sale is not active');
    _;
  }

  // Owner or public sale active modifier
  modifier whenOwnerOrSaleActive() {
    require(
      owner() == _msgSender() || preSaleActive || publicSaleActive,
      'Sale is not active'
    );
    _;
  }

  // Owner or authorized addresses modifier
  modifier whenOwnerOrAuthorizedAddress() {
    require(
      owner() == _msgSender() || authorizedAddresses[_msgSender()],
      'Not authorized'
    );
    _;
  }

  // -- Constructor --//
  constructor(string memory _baseTokenURI, uint8 _counterType)
    ERC721(TOKEN_NAME, TOKEN_SYMBOL)
  {
    baseTokenURI = _baseTokenURI;
    paymentAddress = owner();
    royaltyAddress = owner();
    _tokenIds.setType(_counterType);
  }

  // -- External Functions -- //

  // Start pre sale
  function startPreSale() external onlyOwner {
    _preSaleRound.increment();
    _tokensMintedPerSaleRound.reset();
    preSaleActive = true;
    publicSaleActive = false;
  }

  // End pre sale
  function endPreSale() external onlyOwner {
    preSaleActive = false;
    publicSaleActive = false;
  }

  // Start public sale
  function startPublicSale() external onlyOwner {
    _publicSaleRound.increment();
    _tokensMintedPerSaleRound.reset();
    preSaleActive = false;
    publicSaleActive = true;
  }

  // End public sale
  function endPublicSale() external onlyOwner {
    preSaleActive = false;
    publicSaleActive = false;
  }

  // Support royalty info - See {EIP-2981}: https://eips.ethereum.org/EIPS/eip-2981
  function royaltyInfo(uint256, uint256 _salePrice)
    external
    view
    returns (address receiver, uint256 royaltyAmount)
  {
    return (royaltyAddress, (_salePrice.mul(royaltyBasisPoints)).div(10000));
  }

  // Adds multiple addresses to whitelist
  function addToPresaleWhitelist(address[] memory _addresses)
    external
    onlyOwner
  {
    for (uint256 i = 0; i < _addresses.length; i++) {
      address _address = _addresses[i];
      presaleWhitelist[_address] = true;
    }
  }

  // Removes multiple addresses from whitelist
  function removeFromPresaleWhitelist(address[] memory _addresses)
    external
    onlyOwner
  {
    for (uint256 i = 0; i < _addresses.length; i++) {
      address _address = _addresses[i];
      presaleWhitelist[_address] = false;
    }
  }

  // Mint token - requires amount
  function mint(uint256 _amount) external payable whenOwnerOrSaleActive {
    require(_amount > 0, 'Must mint at least one');

    // Check there enough mints left to mint
    require(_amount <= getMintsLeft(), 'Minting would exceed max supply');

    // Check there are mints left per sale round
    require(
      _amount <= getMintsLeftPerSaleRound(),
      'Minting would exceed max mint amount per sale round'
    );

    // Set cost to mint
    uint256 costToMint = 0;

    bool isOwner = owner() == _msgSender();

    if (!isOwner) {
      // If pre sale is active, make sure user is on whitelist
      if (preSaleActive) {
        require(presaleWhitelist[_msgSender()], 'Must be on whitelist');
      }

      // Set cost to mint
      costToMint = getMintCost(_msgSender()) * _amount;

      // Get current address total balance
      uint256 currentWalletAmount = super.balanceOf(_msgSender());

      // Check current token amount and mint amount is not more than max wallet amount
      require(
        currentWalletAmount.add(_amount) <= maxWalletAmount,
        'Requested amount exceeds maximum mint amount per wallet'
      );
    }

    // Check cost to mint, and if enough ETH is passed to mint
    require(costToMint <= msg.value, 'ETH amount sent is not correct');

    for (uint256 i = 0; i < _amount; i++) {
      // Increment token id
      _tokenIds.increment();

      // Safe mint
      _safeMint(_msgSender(), _tokenIds.current());

      // Increment tokens minted per sale round
      _tokensMintedPerSaleRound.increment();
    }

    // Send mint cost to payment address
    Address.sendValue(payable(paymentAddress), costToMint);

    // Return unused value
    if (msg.value > costToMint) {
      Address.sendValue(payable(_msgSender()), msg.value.sub(costToMint));
    }

    // If tokens minted per sale round hits the max mints per sale round, end pre/public sale
    if (_tokensMintedPerSaleRound.current() >= maxMintsPerSaleRound) {
      preSaleActive = false;
      publicSaleActive = false;
    }
  }

  // Custom mint function - requires token id and reciever address
  // Mint or transfer token id - Used for cross chain bridging
  function customMint(uint256 _tokenId, address _reciever)
    external
    whenOwnerOrAuthorizedAddress
  {
    require(!publicSaleActive && !preSaleActive, 'Sales must be inactive');
    require(
      _tokenId > 0 && _tokenId <= TOTAL_TOKENS,
      'Must pass valid token id'
    );

    if (_exists(_tokenId)) {
      // If token exists, make sure token owner is contract owner
      require(owner() == ownerOf(_tokenId), 'Token is already owned');

      // Transfer from contract owner to reciever
      safeTransferFrom(owner(), _reciever, _tokenId);
    } else {
      // Safe mint
      _safeMint(_reciever, _tokenId);
    }
  }

  // Custom burn function - required token id
  // Transfer token id to contract owner - used for cross chain bridging
  function customBurn(uint256 _tokenId) external whenOwnerOrAuthorizedAddress {
    require(!publicSaleActive && !preSaleActive, 'Sales must be inactive');
    require(
      _tokenId > 0 && _tokenId <= TOTAL_TOKENS,
      'Must pass valid token id'
    );

    require(_exists(_tokenId), 'Nonexistent token');

    // Transfer from token owner to contract owner
    safeTransferFrom(ownerOf(_tokenId), owner(), _tokenId);
  }

  // Adds multiple addresses to authorized addresses
  function addToAuthorizedAddresses(address[] memory _addresses)
    external
    onlyOwner
  {
    for (uint256 i = 0; i < _addresses.length; i++) {
      address _address = _addresses[i];
      authorizedAddresses[_address] = true;
    }
  }

  // Removes multiple addresses from authorized addresses
  function removeFromAuthorizedAddresses(address[] memory _addresses)
    external
    onlyOwner
  {
    for (uint256 i = 0; i < _addresses.length; i++) {
      address _address = _addresses[i];
      authorizedAddresses[_address] = false;
    }
  }

  // Set mint cost
  function setMintCost(uint256 _cost) external onlyOwner {
    mintCost = _cost;
  }

  // Set mint cost contract
  function setERC721HelperContract(address _contract) external onlyOwner {
    if (_contract != address(0)) {
      IERC721Helpers _contCheck = IERC721Helpers(_contract);
      // allow setting to zero address to effectively turn off logic
      require(
        _contCheck.getMintCost(_msgSender()) == 0 ||
          _contCheck.getMintCost(_msgSender()) > 0,
        'Contract does not implement interface'
      );
    }
    mintCostContract = _contract;
  }

  // Set max wallet amount
  function setMaxWalletAmount(uint256 _amount) external onlyOwner {
    maxWalletAmount = _amount;
  }

  // Set max mints per sale round amount
  function setMaxMintsPerSaleRound(uint256 _amount) external onlyOwner {
    maxMintsPerSaleRound = _amount;
  }

  // Reset tokens minted per sale round
  function resetTokensMintedPerSaleRound() external onlyOwner {
    _tokensMintedPerSaleRound.reset();
  }

  // Reset pre sale rounds
  function resetPreSaleRounds() external onlyOwner {
    _preSaleRound.reset();
  }

  // Reset public sale rounds
  function resetPublicSaleRounds() external onlyOwner {
    _publicSaleRound.reset();
  }

  // Set payment address
  function setPaymentAddress(address _address) external onlyOwner {
    paymentAddress = _address;
  }

  // Set royalty wallet address
  function setRoyaltyAddress(address _address) external onlyOwner {
    royaltyAddress = _address;
  }

  // Set royalty basis points
  function setRoyaltyBasisPoints(uint256 _basisPoints) external onlyOwner {
    royaltyBasisPoints = _basisPoints;
    emit RoyaltyBasisPoints(_basisPoints);
  }

  // Set base URI
  function setBaseURI(string memory _uri) external onlyOwner {
    baseTokenURI = _uri;
  }

  //-- Public Functions --//

  // Get mint cost from mint cost contract, or fallback to local mintCost
  function getMintCost(address _address) public view returns (uint256) {
    return
      mintCostContract != address(0)
        ? IERC721Helpers(mintCostContract).getMintCost(_address)
        : mintCost;
  }

  // Get mints left
  function getMintsLeft() public view returns (uint256) {
    uint256 currentSupply = super.totalSupply();
    uint256 counterType = _tokenIds._type;
    uint256 totalTokens = counterType != 0 ? TOTAL_TOKENS.div(2) : TOTAL_TOKENS;
    return totalTokens.sub(currentSupply);
  }

  // Get mints left per sale round
  function getMintsLeftPerSaleRound() public view returns (uint256) {
    return maxMintsPerSaleRound.sub(_tokensMintedPerSaleRound.current());
  }

  // Get circulating supply - current supply minus contract owner supply
  function getCirculatingSupply() public view returns (uint256) {
    uint256 currentSupply = super.totalSupply();
    uint256 ownerSupply = balanceOf(owner());
    return currentSupply.sub(ownerSupply);
  }

  // Get total tokens based on counter type
  function getTotalTokens() public view returns (uint256) {
    uint256 counterType = _tokenIds._type;
    uint256 totalTokens = counterType != 0 ? TOTAL_TOKENS.div(2) : TOTAL_TOKENS;
    return totalTokens;
  }

  // Token URI (baseTokenURI + tokenId)
  function tokenURI(uint256 _tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(_exists(_tokenId), 'Nonexistent token');

    return string(abi.encodePacked(_baseURI(), _tokenId.toString(), '.json'));
  }

  // Contract metadata URI - Support for OpenSea: https://docs.opensea.io/docs/contract-level-metadata
  function contractURI() public view returns (string memory) {
    return string(abi.encodePacked(_baseURI(), 'contract.json'));
  }

  // Override supportsInterface - See {IERC165-supportsInterface}
  function supportsInterface(bytes4 _interfaceId)
    public
    view
    virtual
    override(ERC721, ERC721Enumerable)
    returns (bool)
  {
    return super.supportsInterface(_interfaceId);
  }

  // Pauses all token transfers - See {ERC721Pausable}
  function pause() external virtual onlyOwner {
    _pause();
  }

  // Unpauses all token transfers - See {ERC721Pausable}
  function unpause() external virtual onlyOwner {
    _unpause();
  }

  //-- Internal Functions --//

  // Get base URI
  function _baseURI() internal view override returns (string memory) {
    return baseTokenURI;
  }

  // Before all token transfer
  function _beforeTokenTransfer(
    address _from,
    address _to,
    uint256 _tokenId
  ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) {
    super._beforeTokenTransfer(_from, _to, _tokenId);
  }
}

// File: @openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/ERC721Burnable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../utils/Context.sol";

/**
 * @title ERC721 Burnable Token
 * @dev ERC721 Token that can be irreversibly burned (destroyed).
 */
abstract contract ERC721Burnable is Context, ERC721 {
    /**
     * @dev Burns `tokenId`. See {ERC721-_burn}.
     *
     * Requirements:
     *
     * - The caller must own `tokenId` or be an approved operator.
     */
    function burn(uint256 tokenId) public virtual {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Burnable: caller is not owner nor approved");
        _burn(tokenId);
    }
}

// File: contracts/interfaces/IERC721Helpers.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

/**
 * @dev Interface that can be used to build custom logic for ERC721 tokens..
 */
interface IERC721Helpers {
  /**
   * @dev Returns mint cost in wei.
   */
  function getMintCost(address _address) external view returns (uint256);
}

// File: contracts/utils/Counters.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol)

pragma solidity ^0.8.0;

/**
 * @title Counters
 * @author Lance Whatley
 * @dev Provides counters that can be incremented, decremented or reset, and provides even/odd only logic.
 *
 * Include with `using Counters for Counters.Counter;`
 */
library Counters {
  struct Counter {
    // 0: single digit increment
    // 1: skip one, and only odds
    // 2: skip one, and only get even numbers
    uint8 _type;
    // This variable should never be directly accessed by users of the library: interactions must be restricted to
    // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add
    // this feature: see https://github.com/ethereum/solidity/issues/4637
    uint256 _value; // default: 0
  }

  function setType(Counter storage counter, uint8 counterType) internal {
    require(
      counterType == 0 || counterType == 1 || counterType == 2,
      'invalid type'
    );
    counter._type = counterType;
  }

  function current(Counter storage counter) internal view returns (uint256) {
    return counter._value;
  }

  function increment(Counter storage counter) internal {
    unchecked {
      uint256 incAmount = 1;
      if (counter._type != 0) {
        if (counter._value == 0) {
          incAmount = counter._type == 1 ? 1 : 2;
        } else {
          incAmount = 2;
        }
      }
      counter._value += incAmount;
    }
  }

  function decrement(Counter storage counter) internal {
    uint256 value = counter._value;
    require(value > 0, 'Counter: decrement overflow');
    unchecked {
      uint256 decAmount = counter._type == 0 ? 1 : counter._value == 1 ? 1 : 2;
      counter._value = value - decAmount;
    }
  }

  function reset(Counter storage counter) internal {
    counter._value = 0;
  }
}

// File: contracts/unaudited/NFTMintCostUSD.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';
import '../interfaces/IERC721Helpers.sol';
import '../OKLGWithdrawable.sol';

/**
 * @title NFTMintCostUSD
 * @dev Provide the cost for minting an NFT from Chainlink price feed data
 */
contract NFTMintCostUSD is IERC721Helpers, OKLGWithdrawable {
  AggregatorV3Interface internal priceFeed;
  uint256 public priceUSDCents;

  uint256 public discountAmountPercent = 25;
  mapping(address => bool) public discounted;

  constructor(address _priceFeed, uint256 _priceCents) {
    // https://docs.chain.link/docs/reference-contracts/
    // https://github.com/pcaversaccio/chainlink-price-feed/blob/main/README.md
    priceFeed = AggregatorV3Interface(_priceFeed);
    priceUSDCents = _priceCents;
  }

  function getPriceWei(uint256 _costUSDCents) public view returns (uint256) {
    // Creates a USD balance with 18 decimals
    // NOTE: We multiply by 10**16 because the input is in cents, not USD
    uint256 paymentUSD18 = 10**16 * _costUSDCents;

    // adding back 18 decimals to get returned value in wei
    return (10**18 * paymentUSD18) / getLatestETHPrice();
  }

  /**
   * Returns the latest ETH/USD price with returned value at 18 decimals
   * https://docs.chain.link/docs/get-the-latest-price/
   */
  function getLatestETHPrice() public view returns (uint256) {
    uint8 decimals = priceFeed.decimals();
    (, int256 price, , , ) = priceFeed.latestRoundData();
    return uint256(price) * (10**18 / 10**decimals);
  }

  function setPriceFeed(address _feedContract) external onlyOwner {
    priceFeed = AggregatorV3Interface(_feedContract);
  }

  function setPriceUSD(uint256 _priceUSDCents) external onlyOwner {
    priceUSDCents = _priceUSDCents;
  }

  function setDiscountAmountPercent(uint256 _percent) external onlyOwner {
    require(_percent <= 100, 'cannot be discounted more than 100%');
    discountAmountPercent = _percent;
  }

  function setDiscountedAddresses(address[] memory _wallets, bool _isDiscounted)
    external
    onlyOwner
  {
    for (uint256 _i = 0; _i < _wallets.length; _i++) {
      discounted[_wallets[_i]] = _isDiscounted;
    }
  }

  /**
   * getMintCost: get the amount in Wei of minting an NFT
   */
  function getMintCost(address _wallet)
    external
    view
    override
    returns (uint256)
  {
    if (priceUSDCents == 0) return 0;
    uint256 basePriceWei = getPriceWei(priceUSDCents);
    if (discounted[_wallet]) {
      return basePriceWei - ((basePriceWei * discountAmountPercent) / 100);
    }
    return basePriceWei;
  }
}

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



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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
    );
}

// File: contracts/OKLGSpend.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';
import './interfaces/IOKLGSpend.sol';
import './OKLGWithdrawable.sol';

/**
 * @title OKLGSpend
 * @dev Logic for spending OKLG on products in the product ecosystem.
 */
contract OKLGSpend is IOKLGSpend, OKLGWithdrawable {
  address payable private constant DEAD_WALLET =
    payable(0x000000000000000000000000000000000000dEaD);
  address payable public paymentWallet =
    payable(0x000000000000000000000000000000000000dEaD);

  AggregatorV3Interface internal priceFeed;

  uint256 public totalSpentWei = 0;
  mapping(uint8 => uint256) public defaultProductPriceUSD;
  mapping(address => uint256) public overrideProductPriceUSD;
  mapping(address => bool) public removeCost;
  event Spend(address indexed user, address indexed product, uint256 value);

  constructor(address _linkPriceFeedContract) {
    // https://docs.chain.link/docs/reference-contracts/
    // https://github.com/pcaversaccio/chainlink-price-feed/blob/main/README.md
    priceFeed = AggregatorV3Interface(_linkPriceFeedContract);
  }

  function getProductCostWei(uint256 _productCostUSD)
    public
    view
    returns (uint256)
  {
    // Creates a USD balance with 18 decimals
    uint256 paymentUSD18 = 10**18 * _productCostUSD;

    // adding back 18 decimals to get returned value in wei
    return (10**18 * paymentUSD18) / getLatestETHPrice();
  }

  /**
   * Returns the latest ETH/USD price with returned value at 18 decimals
   * https://docs.chain.link/docs/get-the-latest-price/
   */
  function getLatestETHPrice() public view returns (uint256) {
    uint8 decimals = priceFeed.decimals();
    (, int256 price, , , ) = priceFeed.latestRoundData();
    return uint256(price) * (10**18 / 10**decimals);
  }

  function setPriceFeed(address _feedContract) external onlyOwner {
    priceFeed = AggregatorV3Interface(_feedContract);
  }

  function setPaymentWallet(address _newPaymentWallet) external onlyOwner {
    paymentWallet = payable(_newPaymentWallet);
  }

  function setDefaultProductUSDPrice(uint8 _product, uint256 _priceUSD)
    external
    onlyOwner
  {
    defaultProductPriceUSD[_product] = _priceUSD;
  }

  function setDefaultProductPricesUSDBulk(
    uint8[] memory _productIds,
    uint256[] memory _pricesUSD
  ) external onlyOwner {
    require(
      _productIds.length == _pricesUSD.length,
      'arrays need to be the same length'
    );
    for (uint256 _i = 0; _i < _productIds.length; _i++) {
      defaultProductPriceUSD[_productIds[_i]] = _pricesUSD[_i];
    }
  }

  function setOverrideProductPriceUSD(address _productCont, uint256 _priceUSD)
    external
    onlyOwner
  {
    overrideProductPriceUSD[_productCont] = _priceUSD;
  }

  function setOverrideProductPricesUSDBulk(
    address[] memory _contracts,
    uint256[] memory _pricesUSD
  ) external onlyOwner {
    require(
      _contracts.length == _pricesUSD.length,
      'arrays need to be the same length'
    );
    for (uint256 _i = 0; _i < _contracts.length; _i++) {
      overrideProductPriceUSD[_contracts[_i]] = _pricesUSD[_i];
    }
  }

  function setRemoveCost(address _productCont, bool _isRemoved)
    external
    onlyOwner
  {
    removeCost[_productCont] = _isRemoved;
  }

  /**
   * spendOnProduct: used by an OKLG product for a user to spend native token on usage of a product
   */
  function spendOnProduct(address _payor, uint8 _product)
    external
    payable
    override
  {
    if (removeCost[msg.sender]) return;

    uint256 _productCostUSD = overrideProductPriceUSD[msg.sender] > 0
      ? overrideProductPriceUSD[msg.sender]
      : defaultProductPriceUSD[_product];
    if (_productCostUSD == 0) return;

    uint256 _productCostWei = getProductCostWei(_productCostUSD);

    require(
      msg.value >= _productCostWei,
      'not enough ETH sent to pay for product'
    );
    address payable _paymentWallet = paymentWallet == DEAD_WALLET ||
      paymentWallet == address(0)
      ? payable(owner())
      : paymentWallet;
    _paymentWallet.call{ value: _productCostWei }('');
    totalSpentWei += _productCostWei;
    emit Spend(msg.sender, _payor, _productCostWei);
  }
}

// File: contracts/OKLGAirdropper.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import './interfaces/IOKLGSpend.sol';
import './OKLGProduct.sol';

/**
 * @title OKLGAirdropper
 * @dev Allows sending ERC20 or ERC721 tokens to multiple addresses
 */
contract OKLGAirdropper is OKLGProduct {
  struct Receiver {
    address userAddress;
    uint256 amountOrTokenId;
  }

  constructor(address _tokenAddy, address _spendContractAddy)
    OKLGProduct(uint8(1), _tokenAddy, _spendContractAddy)
  {}

  function bulkSendMainTokens(Receiver[] memory _addressesAndAmounts)
    external
    payable
    returns (bool)
  {
    _payForService(0);

    bool _wasSent = true;

    for (uint256 _i = 0; _i < _addressesAndAmounts.length; _i++) {
      Receiver memory _user = _addressesAndAmounts[_i];
      (bool sent, ) = _user.userAddress.call{ value: _user.amountOrTokenId }(
        ''
      );
      _wasSent = _wasSent == false ? false : sent;
    }
    return _wasSent;
  }

  function bulkSendErc20Tokens(
    address _tokenAddress,
    Receiver[] memory _addressesAndAmounts
  ) external payable returns (bool) {
    _payForService(0);

    IERC20 _token = IERC20(_tokenAddress);
    for (uint256 _i = 0; _i < _addressesAndAmounts.length; _i++) {
      Receiver memory _user = _addressesAndAmounts[_i];
      _token.transferFrom(msg.sender, _user.userAddress, _user.amountOrTokenId);
    }
    return true;
  }

  function bulkSendErc721Tokens(
    address _tokenAddress,
    Receiver[] memory _addressesAndAmounts
  ) external payable returns (bool) {
    _payForService(0);

    IERC721 _token = IERC721(_tokenAddress);
    for (uint256 _i = 0; _i < _addressesAndAmounts.length; _i++) {
      Receiver memory _user = _addressesAndAmounts[_i];
      _token.transferFrom(msg.sender, _user.userAddress, _user.amountOrTokenId);
    }
    return true;
  }
}

// File: contracts/OKLGBuybot.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';
import './OKLGWithdrawable.sol';

/**
 * @title OKLGBuybot
 * @dev Logic for spending OKLG on products in the product ecosystem.
 */
contract OKLGBuybot is OKLGWithdrawable {
  address payable private constant DEAD_WALLET =
    payable(0x000000000000000000000000000000000000dEaD);
  address payable public paymentWallet =
    payable(0x000000000000000000000000000000000000dEaD);

  AggregatorV3Interface internal priceFeed;

  uint256 public totalSpentWei = 0;
  uint256 public paidPricePerDayUsd = 25;
  mapping(address => uint256) public overridePricePerDayUSD;
  mapping(address => bool) public removeCost;
  event SetupBot(
    address indexed user,
    address token,
    string client,
    string channel,
    uint256 expiration
  );
  event SetupBotAdmin(
    address indexed user,
    address token,
    string client,
    string channel,
    uint256 expiration
  );
  event DeleteBot(
    address indexed user,
    address token,
    string client,
    string channel
  );

  struct Buybot {
    address token;
    string client; // telegram, discord, etc.
    string channel;
    bool isPaid;
    uint256 minThresholdUsd;
    // lpPairAltToken?: string; // if blank, assume the other token in the pair is native (ETH, BNB, etc.)
    uint256 expiration; // unix timestamp of expiration, or 0 if no expiration
  }
  mapping(bytes32 => Buybot) public buybotConfigs;
  bytes32[] public buybotConfigsList;

  constructor(address _linkPriceFeedContract) {
    // https://docs.chain.link/docs/reference-contracts/
    // https://github.com/pcaversaccio/chainlink-price-feed/blob/main/README.md
    priceFeed = AggregatorV3Interface(_linkPriceFeedContract);
  }

  /**
   * Returns the latest ETH/USD price with returned value at 18 decimals
   * https://docs.chain.link/docs/get-the-latest-price/
   */
  function getLatestETHPrice() public view returns (uint256) {
    uint8 decimals = priceFeed.decimals();
    (, int256 price, , , ) = priceFeed.latestRoundData();
    return uint256(price) * (10**18 / 10**decimals);
  }

  function setPriceFeed(address _feedContract) external onlyOwner {
    priceFeed = AggregatorV3Interface(_feedContract);
  }

  function setPaymentWallet(address _newPaymentWallet) external onlyOwner {
    paymentWallet = payable(_newPaymentWallet);
  }

  function setOverridePricePerDayUSD(address _wallet, uint256 _priceUSD)
    external
    onlyOwner
  {
    overridePricePerDayUSD[_wallet] = _priceUSD;
  }

  function setOverridePricesPerDayUSDBulk(
    address[] memory _contracts,
    uint256[] memory _pricesUSD
  ) external onlyOwner {
    require(
      _contracts.length == _pricesUSD.length,
      'arrays need to be the same length'
    );
    for (uint256 _i = 0; _i < _contracts.length; _i++) {
      overridePricePerDayUSD[_contracts[_i]] = _pricesUSD[_i];
    }
  }

  function setRemoveCost(address _wallet, bool _isRemoved) external onlyOwner {
    removeCost[_wallet] = _isRemoved;
  }

  function getId(
    address _token,
    string memory _client,
    string memory _channel
  ) public pure returns (bytes32) {
    return sha256(abi.encodePacked(_token, _client, _channel));
  }

  function setupBot(
    address _token,
    string memory _client,
    string memory _channel,
    bool _isPaid,
    uint256 _minThresholdUsd
  ) external payable {
    require(msg.value >= 0, 'must send some ETH to pay for bot');

    uint256 _costPerDayUSD = overridePricePerDayUSD[msg.sender] > 0
      ? overridePricePerDayUSD[msg.sender]
      : paidPricePerDayUsd;

    if (_isPaid && !removeCost[msg.sender]) {
      address payable _paymentWallet = paymentWallet == DEAD_WALLET ||
        paymentWallet == address(0)
        ? payable(owner())
        : paymentWallet;
      _paymentWallet.call{ value: msg.value }('');
      totalSpentWei += msg.value;
      _costPerDayUSD = 0;
    }

    uint256 _daysOfService18 = 30;
    if (_costPerDayUSD > 0) {
      uint256 _costPerDayUSD18 = _costPerDayUSD * 10**18;
      uint256 _ethPriceUSD18 = getLatestETHPrice();
      _daysOfService18 =
        ((msg.value * 10**18) * _ethPriceUSD18) /
        _costPerDayUSD18;
    }

    uint256 _secondsOfService = (_daysOfService18 * 24 * 60 * 60) / 10**18;
    bytes32 _id = getId(_token, _client, _channel);

    Buybot storage _bot = buybotConfigs[_id];
    if (_bot.expiration == 0) {
      buybotConfigsList.push(_id);
    }
    uint256 _start = _bot.expiration < block.timestamp
      ? block.timestamp
      : _bot.expiration;

    _bot.token = _token;
    _bot.isPaid = _isPaid;
    _bot.client = _client;
    _bot.channel = _channel;
    _bot.minThresholdUsd = _minThresholdUsd;
    _bot.expiration = _start + _secondsOfService;
    emit SetupBot(msg.sender, _token, _client, _channel, _bot.expiration);
  }

  function setupBotAdmin(
    address _token,
    string memory _client,
    string memory _channel,
    bool _isPaid,
    uint256 _minThresholdUsd,
    uint256 _expiration
  ) external onlyOwner {
    bytes32 _id = getId(_token, _client, _channel);
    Buybot storage _bot = buybotConfigs[_id];
    if (_bot.expiration == 0) {
      buybotConfigsList.push(_id);
    }
    _bot.token = _token;
    _bot.isPaid = _isPaid;
    _bot.client = _client;
    _bot.channel = _channel;
    _bot.minThresholdUsd = _minThresholdUsd;
    _bot.expiration = _expiration;
    emit SetupBotAdmin(msg.sender, _token, _client, _channel, _bot.expiration);
  }

  function deleteBot(
    address _token,
    string memory _client,
    string memory _channel
  ) external onlyOwner {
    bytes32 _id = getId(_token, _client, _channel);
    delete buybotConfigs[_id];
    for (uint256 _i = 0; _i < buybotConfigsList.length; _i++) {
      if (buybotConfigsList[_i] == _id) {
        buybotConfigsList[_i] = buybotConfigsList[buybotConfigsList.length - 1];
        buybotConfigsList.pop();
      }
    }
    emit DeleteBot(msg.sender, _token, _client, _channel);
  }
}

// File: contracts/MTGYOKLGSwap.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';

/**
 * @title MTGYOKLGSwap
 * @dev Swap MTGY for OKLG on BSC
 */
contract MTGYOKLGSwap is Ownable {
  IERC20 private mtgy = IERC20(0x025c9f1146d4d94F8F369B9d98104300A3c8ca23);
  IERC20 private oklg = IERC20(0x55E8b37a3c43B049deDf56C77f462Db095108651);

  uint8 public mtgyOklgRatio = 120;

  function swap() external {
    uint256 mtgyBalance = mtgy.balanceOf(msg.sender);
    require(mtgyBalance > 0, 'must have a MTGY balance to swap for OKLG');

    uint256 oklgToTransfer = (mtgyBalance * mtgyOklgRatio) / 10**9; // MTGY has 18 decimals, OKLG has 9 decimals
    require(
      oklg.balanceOf(address(this)) >= oklgToTransfer,
      'not enough OKLG liquidity to execute swap'
    );

    mtgy.transferFrom(msg.sender, address(this), mtgyBalance);
    oklg.transfer(msg.sender, oklgToTransfer);
  }

  function changeRatio(uint8 _newRatio) external onlyOwner {
    mtgyOklgRatio = _newRatio;
  }

  function withdrawTokens(address _tokenAddy, uint256 _amount)
    external
    onlyOwner
  {
    IERC20 _token = IERC20(_tokenAddy);
    _amount = _amount > 0 ? _amount : _token.balanceOf(address(this));
    require(_amount > 0, 'make sure there is a balance available to withdraw');
    _token.transfer(owner(), _amount);
  }
}

// File: contracts/KetherNFTLoaner.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';

interface IKetherNFT {
  function ownerOf(uint256 _tokenId) external view returns (address);

  function transferFrom(
    address _from,
    address _to,
    uint256 _tokenId
  ) external payable;

  function publish(
    uint256 _idx,
    string calldata _link,
    string calldata _image,
    string calldata _title,
    bool _NSFW
  ) external;
}

/**
 * @title KetherNFTLoaner
 * @dev Support loaning KetherNFT plots of ad space to others over a period of time
 */
contract KetherNFTLoaner is Ownable {
  using SafeMath for uint256;

  uint256 private constant _1ETH = 1 ether;
  uint256 public loanServiceCharge = _1ETH.div(100).mul(5);
  uint256 public loanChargePerDay = _1ETH.div(1000);
  uint16 public maxLoanDurationDays = 30;
  uint8 public loanPercentageCharge = 10;
  IKetherNFT private _ketherNft;

  struct PlotOwner {
    address owner;
    uint256 overrideLoanChargePerDay;
    uint16 overrideMaxLoanDurationDays;
    uint256 totalFeesCollected;
  }

  struct PlotLoan {
    address loaner;
    uint256 start;
    uint256 end;
    uint256 totalFee;
  }

  struct PublishParams {
    string link;
    string image;
    string title;
    bool NSFW;
  }

  mapping(uint256 => PlotOwner) public owners;
  mapping(uint256 => PlotLoan) public loans;

  event AddPlot(
    uint256 indexed idx,
    address owner,
    uint256 overridePerDayCharge,
    uint16 overrideMaxLoanDays
  );
  event UpdatePlot(
    uint256 indexed idx,
    uint256 overridePerDayCharge,
    uint16 overrideMaxLoanDays
  );
  event RemovePlot(uint256 indexed idx, address owner);
  event LoanPlot(uint256 indexed idx, address loaner);
  event Transfer(address to, uint256 idx);

  constructor(address _ketherNFTAddress) {
    _ketherNft = IKetherNFT(_ketherNFTAddress);
  }

  function addPlot(
    uint256 _idx,
    uint256 _overridePerDayCharge,
    uint16 _overrideMaxDays
  ) external payable {
    require(
      msg.sender == _ketherNft.ownerOf(_idx),
      'You need to be the owner of the plot to loan it out.'
    );
    require(
      msg.value >= loanServiceCharge,
      'You must send the appropriate service charge to support loaning your plot.'
    );
    payable(owner()).call{ value: msg.value }('');
    _ketherNft.transferFrom(msg.sender, address(this), _idx);
    owners[_idx] = PlotOwner({
      owner: msg.sender,
      overrideLoanChargePerDay: _overridePerDayCharge,
      overrideMaxLoanDurationDays: _overrideMaxDays,
      totalFeesCollected: 0
    });
    emit AddPlot(_idx, msg.sender, _overridePerDayCharge, _overrideMaxDays);
  }

  function updatePlot(
    uint256 _idx,
    uint256 _overridePerDayCharge,
    uint16 _overrideMaxDays
  ) external {
    PlotOwner storage _owner = owners[_idx];
    require(
      msg.sender == _owner.owner,
      'You must be the plot owner to update information about it.'
    );
    _owner.overrideLoanChargePerDay = _overridePerDayCharge;
    _owner.overrideMaxLoanDurationDays = _overrideMaxDays;
    emit UpdatePlot(_idx, _overridePerDayCharge, _overrideMaxDays);
  }

  function removePlot(uint256 _idx) external payable {
    address _owner = owners[_idx].owner;
    require(
      msg.sender == _owner,
      'You must be the original owner of the plot to remove it from the loan contract.'
    );

    // If there is an active loan, make sure the owner of the plot who's removing pays the loaner
    // back a the full amount of the original loan fee for breaking the loan agreement
    if (hasActiveLoan(_idx)) {
      PlotLoan storage _loan = loans[_idx];
      uint256 _loanFee = _loan.totalFee;
      require(
        msg.value >= _loanFee,
        'You need to reimburse the loaner for breaking the loan agreement early.'
      );
      payable(_loan.loaner).call{ value: _loanFee }('');
      _loan.end = 0;
    }

    _ketherNft.transferFrom(address(this), msg.sender, _idx);
    emit RemovePlot(_idx, msg.sender);
  }

  function loanPlot(
    uint256 _idx,
    uint16 _numDays,
    PublishParams memory _publishParams
  ) external payable {
    require(_numDays > 0, 'You must loan the plot for at least a day.');

    PlotOwner storage _plotOwner = owners[_idx];
    PlotLoan memory _loan = loans[_idx];
    require(_loan.end < block.timestamp, 'Plot is currently being loaned.');

    _ensureValidLoanDays(_plotOwner, _numDays);
    _ensureValidLoanCharge(_plotOwner, _numDays);

    uint256 _serviceCharge = msg.value.mul(uint256(loanPercentageCharge)).div(
      100
    );
    uint256 _plotOwnerCharge = msg.value.sub(_serviceCharge);

    payable(owner()).call{ value: _serviceCharge }('');
    payable(_plotOwner.owner).call{ value: _plotOwnerCharge }('');

    _plotOwner.totalFeesCollected += _plotOwnerCharge;
    loans[_idx] = PlotLoan({
      loaner: msg.sender,
      start: block.timestamp,
      end: block.timestamp.add(_daysToSeconds(_numDays)),
      totalFee: msg.value
    });
    _publish(_idx, _publishParams);
    emit LoanPlot(_idx, msg.sender);
  }

  function publish(uint256 _idx, PublishParams memory _publishParams) external {
    PlotOwner memory _owner = owners[_idx];
    PlotLoan memory _loan = loans[_idx];

    bool _hasActiveLoan = hasActiveLoan(_idx);
    if (_hasActiveLoan) {
      require(
        msg.sender == _loan.loaner,
        'Must be the current loaner to update published information.'
      );
    } else {
      require(
        msg.sender == _owner.owner,
        'Must be the owner to update published information.'
      );
    }

    _publish(_idx, _publishParams);
  }

  function transfer(address _to, uint256 _idx) external {
    PlotOwner storage _owner = owners[_idx];
    require(
      msg.sender == _owner.owner,
      'You must own the current plot to transfer it.'
    );
    _owner.owner = _to;
    emit Transfer(_to, _idx);
  }

  function hasActiveLoan(uint256 _idx) public view returns (bool) {
    PlotLoan memory _loan = loans[_idx];
    if (_loan.loaner == address(0)) {
      return false;
    }
    return _loan.end > block.timestamp;
  }

  function setLoanServiceCharge(uint256 _amountWei) external onlyOwner {
    loanServiceCharge = _amountWei;
  }

  function setLoanChargePerDay(uint256 _amountWei) external onlyOwner {
    loanChargePerDay = _amountWei;
  }

  function setMaxLoanDurationDays(uint16 _numDays) external onlyOwner {
    maxLoanDurationDays = _numDays;
  }

  function setLoanPercentageCharge(uint8 _percentage) external onlyOwner {
    require(_percentage <= 100, 'Must be between 0 and 100');
    loanPercentageCharge = _percentage;
  }

  function _daysToSeconds(uint256 _days) private pure returns (uint256) {
    return _days.mul(24).mul(60).mul(60);
  }

  function _ensureValidLoanDays(PlotOwner memory _owner, uint16 _numDays)
    private
    view
  {
    uint16 _maxNumDays = _owner.overrideMaxLoanDurationDays > 0
      ? _owner.overrideMaxLoanDurationDays
      : maxLoanDurationDays;
    require(
      _numDays <= _maxNumDays,
      'You cannot loan this plot for this long.'
    );
  }

  function _ensureValidLoanCharge(PlotOwner memory _owner, uint16 _numDays)
    private
    view
  {
    uint256 _perDayCharge = _owner.overrideLoanChargePerDay > 0
      ? _owner.overrideLoanChargePerDay
      : loanChargePerDay;
    uint256 _loanCharge = _perDayCharge.mul(uint256(_numDays));
    require(
      msg.value >= _loanCharge,
      'Make sure you send the appropriate amount of ETH to process your loan.'
    );
  }

  function _publish(uint256 _idx, PublishParams memory _publishParams) private {
    _ketherNft.publish(
      _idx,
      _publishParams.link,
      _publishParams.image,
      _publishParams.title,
      _publishParams.NSFW
    );
  }
}

// File: contracts/IsERC20HODLer.sol



// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './interfaces/IConditional.sol';

contract IsERC20HODLer is IConditional, Ownable {
  address public tokenContract;
  uint256 public numSecondsForBooster = 60 * 60 * 24 * 7; // 7 days
  mapping(address => uint256) public userBalances;
  mapping(address => uint256) public userBalTimestamp;

  constructor(address _tokenContract) {
    tokenContract = _tokenContract;
  }

  function passesTest(address wallet) external view override returns (bool) {
    uint256 userBal = IERC20(tokenContract).balanceOf(wallet);
    return
      userBal > 0 &&
      userBalances[wallet] > 0 &&
      userBalTimestamp[wallet] > 0 &&
      userBal >= userBalances[wallet] &&
      block.timestamp > userBalTimestamp[wallet] + numSecondsForBooster;
  }

  function setBalanceAndTimestamp() external {
    userBalances[msg.sender] = IERC20(tokenContract).balanceOf(msg.sender);
    userBalTimestamp[msg.sender] = block.timestamp;
  }

  function setTokenAddress(address _tokenContract) external onlyOwner {
    tokenContract = _tokenContract;
  }

  function setNumSecondsForBooster(uint256 _seconds) external onlyOwner {
    numSecondsForBooster = _seconds;
  }
}

// File: contracts/HasERC721Balance.sol



// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC721.sol';
import './interfaces/IConditional.sol';

contract HasERC721Balance is IConditional, Ownable {
  address public nftContract;
  uint256 public minTokenBalance = 1;

  constructor(address _nftContract) {
    nftContract = _nftContract;
  }

  function passesTest(address wallet) external view override returns (bool) {
    return IERC721(nftContract).balanceOf(wallet) >= minTokenBalance;
  }

  function setTokenAddress(address _nftContract) external onlyOwner {
    nftContract = _nftContract;
  }

  function setMinTokenBalance(uint256 _newMin) external onlyOwner {
    minTokenBalance = _newMin;
  }
}

// File: contracts/HasERC20Balance.sol



// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './interfaces/IConditional.sol';

contract HasERC20Balance is IConditional, Ownable {
  address public tokenContract;
  uint256 public minTokenBalance = 1;

  constructor(address _tokenContract) {
    tokenContract = _tokenContract;
  }

  function passesTest(address wallet) external view override returns (bool) {
    return IERC20(tokenContract).balanceOf(wallet) >= minTokenBalance;
  }

  function setTokenAddress(address _tokenContract) external onlyOwner {
    tokenContract = _tokenContract;
  }

  function setMinTokenBalance(uint256 _newMin) external onlyOwner {
    minTokenBalance = _newMin;
  }
}

// File: contracts/OKLGAtomicSwapInstance.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './OKLGProduct.sol';

interface IERC20Decimals is IERC20 {
  function decimals() external view returns (uint8);
}

/**
 * @title OKLGAtomicSwapInstance
 * @dev This is the main contract that supports holding metadata for OKLG atomic inter and intrachain swapping
 */
contract OKLGAtomicSwapInstance is OKLGProduct {
  IERC20Decimals private _token;

  address public tokenOwner;
  address payable public oracleAddress;
  uint256 public maxSwapAmount;
  uint8 public targetTokenDecimals;
  uint256 public minimumGasForOperation = 2 * 10**15; // 2 finney (0.002 ETH)
  bool public isActive = true;

  struct Swap {
    bytes32 id;
    uint256 origTimestamp;
    uint256 currentTimestamp;
    bool isOutbound;
    bool isComplete;
    bool isRefunded;
    bool isSendGasFunded;
    address swapAddress;
    uint256 amount;
  }

  mapping(bytes32 => Swap) public swaps;
  mapping(address => Swap) public lastUserSwap;

  event ReceiveTokensFromSource(
    bytes32 indexed id,
    uint256 origTimestamp,
    address sender,
    uint256 amount
  );

  event SendTokensToDestination(
    bytes32 indexed id,
    address receiver,
    uint256 amount
  );

  event RefundTokensToSource(
    bytes32 indexed id,
    address sender,
    uint256 amount
  );

  event TokenOwnerUpdated(address previousOwner, address newOwner);

  constructor(
    address _costToken,
    address _spendAddress,
    address _oracleAddress,
    address _tokenOwner,
    address _tokenAddy,
    uint8 _targetTokenDecimals,
    uint256 _maxSwapAmount
  ) OKLGProduct(uint8(7), _costToken, _spendAddress) {
    oracleAddress = payable(_oracleAddress);
    tokenOwner = _tokenOwner;
    maxSwapAmount = _maxSwapAmount;
    targetTokenDecimals = _targetTokenDecimals;
    _token = IERC20Decimals(_tokenAddy);
  }

  function getSwapTokenAddress() external view returns (address) {
    return address(_token);
  }

  function setActiveState(bool _isActive) external {
    require(
      msg.sender == owner() || msg.sender == tokenOwner,
      'setActiveState user must be contract creator'
    );
    isActive = _isActive;
  }

  function setOracleAddress(address _oracleAddress) external onlyOwner {
    oracleAddress = payable(_oracleAddress);
    transferOwnership(oracleAddress);
  }

  function setTargetTokenDecimals(uint8 _decimals) external onlyOwner {
    targetTokenDecimals = _decimals;
  }

  function setTokenOwner(address newOwner) external {
    require(
      msg.sender == tokenOwner,
      'user must be current token owner to change it'
    );
    address previousOwner = tokenOwner;
    tokenOwner = newOwner;
    emit TokenOwnerUpdated(previousOwner, newOwner);
  }

  function withdrawTokens(uint256 _amount) external {
    require(
      msg.sender == tokenOwner,
      'withdrawTokens user must be token owner'
    );
    _token.transfer(msg.sender, _amount);
  }

  function setSwapCompletionStatus(bytes32 _id, bool _isComplete)
    external
    onlyOwner
  {
    swaps[_id].isComplete = _isComplete;
  }

  function setMinimumGasForOperation(uint256 _amountGas) external onlyOwner {
    minimumGasForOperation = _amountGas;
  }

  function receiveTokensFromSource(uint256 _amount)
    external
    payable
    returns (bytes32, uint256)
  {
    require(isActive, 'this atomic swap instance is not active');
    require(
      msg.value >= minimumGasForOperation,
      'you must also send enough gas to cover the target transaction'
    );
    require(
      maxSwapAmount == 0 || _amount <= maxSwapAmount,
      'trying to send more than maxSwapAmount'
    );

    _payForService(minimumGasForOperation);

    if (minimumGasForOperation > 0) {
      oracleAddress.call{ value: minimumGasForOperation }('');
    }
    _token.transferFrom(msg.sender, address(this), _amount);

    uint256 _ts = block.timestamp;
    bytes32 _id = sha256(abi.encodePacked(msg.sender, _ts, _amount));
    swaps[_id] = Swap({
      id: _id,
      origTimestamp: _ts,
      currentTimestamp: _ts,
      isOutbound: false,
      isComplete: false,
      isRefunded: false,
      isSendGasFunded: false,
      swapAddress: msg.sender,
      amount: _amount
    });
    lastUserSwap[msg.sender] = swaps[_id];
    emit ReceiveTokensFromSource(_id, _ts, msg.sender, _amount);
    return (_id, _ts);
  }

  function unsetLastUserSwap(address _addy) external onlyOwner {
    delete lastUserSwap[_addy];
  }

  // msg.sender must be the user who originally created the swap.
  // Otherwise, the unique identifier will not match from the originally
  // sending txn.
  //
  // NOTE: We're aware this function can be spoofed by creating a sha256 hash of msg.sender's address
  // and _origTimestamp, but it's important to note refundTokensFromSource and sendTokensToDestination
  // can only be executed by the owner/oracle. Therefore validation should be done by the oracle before
  // executing those and the only possibility of a vulnerability is if someone has compromised the oracle account.
  function fundSendToDestinationGas(
    bytes32 _id,
    uint256 _origTimestamp,
    uint256 _amount
  ) external payable {
    require(
      msg.value >= minimumGasForOperation,
      'you must send enough gas to cover the send transaction'
    );
    require(
      _id == sha256(abi.encodePacked(msg.sender, _origTimestamp, _amount)),
      "we don't recognize this swap"
    );
    if (minimumGasForOperation > 0) {
      oracleAddress.call{ value: minimumGasForOperation }('');
    }
    swaps[_id] = Swap({
      id: _id,
      origTimestamp: _origTimestamp,
      currentTimestamp: block.timestamp,
      isOutbound: true,
      isComplete: false,
      isRefunded: false,
      isSendGasFunded: true,
      swapAddress: msg.sender,
      amount: _amount
    });
  }

  // This must be called AFTER fundSendToDestinationGas has been executed
  // for this txn to fund this send operation
  function refundTokensFromSource(bytes32 _id) external {
    require(isActive, 'this atomic swap instance is not active');

    Swap storage swap = swaps[_id];

    _confirmSwapExistsGasFundedAndSenderValid(swap);
    swap.isRefunded = true;
    _token.transfer(swap.swapAddress, swap.amount);
    emit RefundTokensToSource(_id, swap.swapAddress, swap.amount);
  }

  // This must be called AFTER fundSendToDestinationGas has been executed
  // for this txn to fund this send operation
  function sendTokensToDestination(bytes32 _id) external returns (bytes32) {
    require(isActive, 'this atomic swap instance is not active');

    Swap storage swap = swaps[_id];

    _confirmSwapExistsGasFundedAndSenderValid(swap);

    // handle if this token and target chain token in bridge have different decimals
    // current decimals = 9 -- 100 tokens == 100000000000
    // target decimals = 18 -- 100 tokens == 100000000000000000000
    // to get current amount to transfer, need to multiply by ratio of 10^currentDecimals / 10^targetDecimals
    uint256 _swapAmount = swap.amount;
    if (targetTokenDecimals > 0) {
      _swapAmount =
        (_swapAmount * 10**_token.decimals()) /
        10**targetTokenDecimals;
    }
    _token.transfer(swap.swapAddress, _swapAmount);

    swap.currentTimestamp = block.timestamp;
    swap.isComplete = true;
    emit SendTokensToDestination(_id, swap.swapAddress, _swapAmount);
    return _id;
  }

  function _confirmSwapExistsGasFundedAndSenderValid(Swap memory swap)
    private
    view
    onlyOwner
  {
    // functions that call this should only be called by the current owner
    // or oracle address as they will do the appropriate validation beforehand
    // to confirm the receiving swap is valid before sending tokens to the user.
    require(
      swap.origTimestamp > 0 && swap.amount > 0,
      'swap does not exist yet.'
    );
    // We're just validating here that the swap has not been
    // completed and gas has been funded before moving forward.
    require(
      !swap.isComplete && !swap.isRefunded && swap.isSendGasFunded,
      'swap has already been completed, refunded, or gas has not been funded'
    );
  }
}

// File: contracts/OKLGAtomicSwap.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/interfaces/IERC20.sol';
import './OKLGProduct.sol';
import './OKLGAtomicSwapInstance.sol';

/**
 * @title OKLGAtomicSwap
 * @dev This is the main contract that supports holding metadata for OKLG atomic inter and intrachain swapping
 */
contract OKLGAtomicSwap is OKLGProduct {
  struct TargetSwapInfo {
    bytes32 id;
    uint256 timestamp;
    uint256 index;
    address creator;
    address sourceContract;
    string targetNetwork;
    address targetContract;
    uint8 targetDecimals;
    bool isActive;
  }

  uint256 public swapCreationGasLoadAmount = 1 * 10**16; // 10 finney (0.01 ether)
  address payable public oracleAddress;

  // mapping with "0xSourceContractInstance" => targetContractInstanceInfo that
  // our oracle can query and get the target network contract as needed.
  TargetSwapInfo[] public targetSwapContracts;
  mapping(address => TargetSwapInfo) public targetSwapContractsIndexed;
  mapping(address => TargetSwapInfo) private lastUserCreatedContract;

  // event CreateSwapContract(
  //   uint256 timestamp,
  //   address contractAddress,
  //   string targetNetwork,
  //   address indexed targetContract,
  //   address creator
  // );

  constructor(
    address _tokenAddress,
    address _spendAddress,
    address _oracleAddress
  ) OKLGProduct(uint8(6), _tokenAddress, _spendAddress) {
    oracleAddress = payable(_oracleAddress);
  }

  function updateSwapCreationGasLoadAmount(uint256 _amount) external onlyOwner {
    swapCreationGasLoadAmount = _amount;
  }

  function getLastCreatedContract(address _addy)
    external
    view
    returns (TargetSwapInfo memory)
  {
    return lastUserCreatedContract[_addy];
  }

  function setOracleAddress(address _oracleAddress, bool _changeAll)
    external
    onlyOwner
  {
    oracleAddress = payable(_oracleAddress);
    if (_changeAll) {
      for (uint256 _i = 0; _i < targetSwapContracts.length; _i++) {
        OKLGAtomicSwapInstance _contract = OKLGAtomicSwapInstance(
          targetSwapContracts[_i].sourceContract
        );
        _contract.setOracleAddress(oracleAddress);
      }
    }
  }

  function getAllSwapContracts()
    external
    view
    returns (TargetSwapInfo[] memory)
  {
    return targetSwapContracts;
  }

  function updateSwapContract(
    uint256 _createdBlockTimestamp,
    address _sourceContract,
    string memory _targetNetwork,
    address _targetContract,
    uint8 _targetDecimals,
    bool _isActive
  ) external {
    TargetSwapInfo storage swapContInd = targetSwapContractsIndexed[
      _sourceContract
    ];
    TargetSwapInfo storage swapCont = targetSwapContracts[swapContInd.index];

    require(
      msg.sender == owner() ||
        msg.sender == swapCont.creator ||
        msg.sender == oracleAddress,
      'updateSwapContract must be contract creator'
    );

    bytes32 _id = sha256(
      abi.encodePacked(swapCont.creator, _createdBlockTimestamp)
    );
    require(
      address(0) != _targetContract,
      'target contract cannot be 0 address'
    );
    require(
      swapCont.id == _id && swapContInd.id == _id,
      "we don't recognize the info you sent with the swap"
    );

    swapCont.targetNetwork = _targetNetwork;
    swapContInd.targetNetwork = swapCont.targetNetwork;

    swapCont.targetContract = _targetContract;
    swapContInd.targetContract = swapCont.targetContract;

    swapCont.targetDecimals = _targetDecimals;
    swapContInd.targetDecimals = swapCont.targetDecimals;
    // TODO: if the decimals are changed from the original execution,
    // should also execute #setTargetTokenDecimals on the instance contract.

    swapCont.isActive = _isActive;
    swapContInd.isActive = swapCont.isActive;
  }

  function createNewAtomicSwapContract(
    address _tokenAddy,
    uint256 _tokenSupply,
    uint256 _maxSwapAmount,
    string memory _targetNetwork,
    address _targetContract,
    uint8 _targetDecimals
  ) external payable returns (uint256, address) {
    _payForService(swapCreationGasLoadAmount);
    oracleAddress.call{ value: swapCreationGasLoadAmount }('');

    IERC20 _token = IERC20(_tokenAddy);
    OKLGAtomicSwapInstance _contract = new OKLGAtomicSwapInstance(
      getTokenAddress(),
      getSpendAddress(),
      oracleAddress,
      msg.sender,
      _tokenAddy,
      _targetDecimals,
      _maxSwapAmount
    );

    if (_tokenSupply > 0) {
      _token.transferFrom(msg.sender, address(_contract), _tokenSupply);
    }
    _contract.transferOwnership(oracleAddress);

    uint256 _ts = block.timestamp;
    TargetSwapInfo memory newContract = TargetSwapInfo({
      id: sha256(abi.encodePacked(msg.sender, _ts)),
      timestamp: _ts,
      index: targetSwapContracts.length,
      creator: msg.sender,
      sourceContract: address(_contract),
      targetNetwork: _targetNetwork,
      targetContract: _targetContract,
      targetDecimals: _targetDecimals,
      isActive: true
    });

    targetSwapContracts.push(newContract);
    targetSwapContractsIndexed[address(_contract)] = newContract;
    lastUserCreatedContract[msg.sender] = newContract;
    // emit CreateSwapContract(
    //   _ts,
    //   address(_contract),
    //   _targetNetwork,
    //   _targetContract,
    //   msg.sender
    // );
    return (_ts, address(_contract));
  }
}

// File: contracts/OKLGETHSplitter.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import './OKLGWithdrawable.sol';

contract OKLGETHSplitter is OKLGWithdrawable {
  struct Receiver {
    address wallet;
    uint8 percent;
  }

  Receiver[] public receivers;

  function numberOfReceivers() external view returns (uint256) {
    return receivers.length;
  }

  function setReceivers(address[] memory _wallets, uint8[] memory _percents)
    external
    onlyOwner
  {
    require(
      _wallets.length == _percents.length,
      'wallets and percents should match and correspond.'
    );
    delete receivers;
    uint8 total = 0;
    for (uint256 _i = 0; _i < _wallets.length; _i++) {
      receivers.push(
        Receiver({ wallet: _wallets[_i], percent: _percents[_i] })
      );
      total += _percents[_i];
    }
    require(
      total <= 100,
      'total percentages should be less than or equal to 100'
    );
  }

  receive() external payable {
    if (receivers.length > 0) {
      for (uint256 _i = 0; _i < receivers.length; _i++) {
        Receiver memory r = receivers[_i];
        payable(r.wallet).call{ value: (msg.value * r.percent) / 100 }('');
      }
    }
  }
}