// Contract: CHI
// Target function: function supportsInterface(bytes4 _interfaceId)
        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}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, 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) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, 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) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev Moves `amount` of tokens from `from` to `to`.
     *
     * 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:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     */
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
        }
        _balances[to] += amount;

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, 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 Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - 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 {}
}
contract CHIStake is ERC721Enumerable, Ownable {
  using Strings for uint256;
  using Counters for Counters.Counter;

  uint256 private constant ONE_YEAR = 365 days;
  uint256 private constant FOUR_DAYS = 4 days;
  uint16 private constant PERCENT_DENOMENATOR = 10000;

  IERC20 internal stakeToken;
  ICHI internal CHI;
  ICHIVester internal vester;
  ICHISRewards internal rewards;

  struct AprLock {
    uint16 apr;
    uint256 lockTime;
  }
  AprLock[] internal _aprLockOptions;

  struct Stake {
    uint256 created;
    uint256 amountStaked;
    uint256 amountCHIBaseEarn;
    uint16 apr;
    uint256 lockTime;
  }
  // tokenId => Stake
  mapping(uint256 => Stake) public stakes;
  // tokenId => amount
  mapping(uint256 => uint256) public yieldClaimed;
  // tokenId => timestamp
  mapping(uint256 => uint256) public lastClaim;
  // tokenId => boolean
  mapping(uint256 => bool) public isBlacklisted;

  Counters.Counter internal _ids;
  string private baseTokenURI; // baseTokenURI can point to IPFS folder like https://ipfs.io/ipfs/{cid}/ while
  address public paymentAddress;
  address public royaltyAddress;

  // Royalties basis points (percentage using 2 decimals - 1000 = 100, 500 = 50, 0 = 0)
  uint256 private royaltyBasisPoints = 50; // 5%

  // array of all the NFT token IDs owned by a user
  mapping(address => uint256[]) public allUserOwned;
  // the index in the token ID array at allUserOwned to save gas on operations
  mapping(uint256 => uint256) public ownedIndex;

  mapping(uint256 => uint256) public tokenMintedAt;
  mapping(uint256 => uint256) public tokenLastTransferred;

  event StakeTokens(
    address indexed user,
    uint256 indexed tokenId,
    uint256 amountStaked,
    uint256 lockOptionIndex
  );
  event UnstakeTokens(address indexed user, uint256 indexed tokenId);
  event SetAnnualApr(uint256 indexed newApr);
  event SetPaymentAddress(address indexed user);
  event SetRoyaltyAddress(address indexed user);
  event SetRoyaltyBasisPoints(uint256 indexed _royaltyBasisPoints);
  event SetBaseTokenURI(string indexed newUri);
  event AddAprLockOption(uint16 indexed apr, uint256 lockTime);
  event RemoveAprLockOption(
    uint256 indexed index,
    uint16 indexed apr,
    uint256 lockTime
  );
  event UpdateAprLockOption(
    uint256 indexed index,
    uint16 indexed oldApr,
    uint256 oldLockTime,
    uint16 newApr,
    uint256 newLockTime
  );
  event SetTokenBlacklist(uint256 indexed tokenId, bool isBlacklisted);

  constructor(
    string memory _name,
    string memory _symbol,
    address _stakeToken,
    address _CHI,
    address _vester,
    address _rewards,
    string memory _baseTokenURI
  ) ERC721(_name, _symbol) {
    stakeToken = IERC20(_stakeToken);
    CHI = ICHI(_CHI);
    vester = ICHIVester(_vester);
    rewards = ICHISRewards(_rewards);
    baseTokenURI = _baseTokenURI;
  }

  function stake(uint256 _amount, uint256 _lockOptIndex) external virtual {
    _stake(msg.sender, _amount, _amount, _lockOptIndex, true);
  }

  function _stake(
    address _user,
    uint256 _amountStaked,
    uint256 _amountCHIBaseEarn,
    uint256 _lockOptIndex,
    bool _transferStakeToken
  ) internal {
    require(_lockOptIndex < _aprLockOptions.length, 'invalid lock option');
    _amountStaked = _amountStaked == 0
      ? stakeToken.balanceOf(_user)
      : _amountStaked;
    _amountCHIBaseEarn = _amountCHIBaseEarn == 0
      ? _amountStaked
      : _amountCHIBaseEarn;
    require(
      _amountStaked > 0 && _amountCHIBaseEarn > 0,
      'must stake and be earning at least some tokens'
    );
    if (_transferStakeToken) {
      stakeToken.transferFrom(_user, address(this), _amountStaked);
    }

    _ids.increment();
    stakes[_ids.current()] = Stake({
      created: block.timestamp,
      amountStaked: _amountStaked,
      amountCHIBaseEarn: _amountCHIBaseEarn,
      apr: _aprLockOptions[_lockOptIndex].apr,
      lockTime: _aprLockOptions[_lockOptIndex].lockTime
    });
    _safeMint(_user, _ids.current());
    tokenMintedAt[_ids.current()] = block.timestamp;

    emit StakeTokens(_user, _ids.current(), _amountStaked, _lockOptIndex);
  }

  function unstake(uint256 _tokenId) public {
    address _user = msg.sender;
    Stake memory _tokenStake = stakes[_tokenId];
    require(
      _user == ownerOf(_tokenId),
      'only the owner of the staked tokens can unstake'
    );
    bool _isUnstakingEarly = block.timestamp <
      _tokenStake.created + _tokenStake.lockTime;

    // send back original tokens staked
    // if unstaking early based on lock period, only get a portion back
    if (_isUnstakingEarly) {
      uint256 _timeStaked = block.timestamp - _tokenStake.created;
      uint256 _earnedAmount = (_tokenStake.amountStaked * _timeStaked) /
        _tokenStake.lockTime;
      stakeToken.transfer(_user, _earnedAmount);
      if (address(stakeToken) == address(CHI)) {
        CHI.burn(_tokenStake.amountStaked - _earnedAmount);
      } else {
        stakeToken.transfer(owner(), _tokenStake.amountStaked - _earnedAmount);
      }
    } else {
      stakeToken.transfer(_user, _tokenStake.amountStaked);
    }

    // check and create new vest if yield available to be claimed
    uint256 _totalEarnedAmount = getTotalEarnedAmount(_tokenId);
    if (_totalEarnedAmount > yieldClaimed[_tokenId]) {
      _createVestAndMint(_user, _totalEarnedAmount - yieldClaimed[_tokenId]);
    }

    // this NFT is useless after the user unstakes
    _burn(_tokenId);

    emit UnstakeTokens(_user, _tokenId);
  }

  function unstakeMulti(uint256[] memory _tokenIds) external {
    for (uint256 i = 0; i < _tokenIds.length; i++) {
      unstake(_tokenIds[i]);
    }
  }

  function claimAndVestRewards(uint256 _tokenId) public {
    require(!isBlacklisted[_tokenId], 'blacklisted NFT');

    // user can only claim and vest rewards up to once a week
    require(block.timestamp > lastClaim[_tokenId] + FOUR_DAYS);
    lastClaim[_tokenId] = block.timestamp;

    uint256 _totalEarnedAmount = getTotalEarnedAmount(_tokenId);
    require(
      _totalEarnedAmount > yieldClaimed[_tokenId],
      'must have some yield to claim'
    );
    _createVestAndMint(
      ownerOf(_tokenId),
      _totalEarnedAmount - yieldClaimed[_tokenId]
    );
    yieldClaimed[_tokenId] = _totalEarnedAmount;
  }

  function claimAndVestRewardsMulti(uint256[] memory _tokenIds) external {
    for (uint256 i = 0; i < _tokenIds.length; i++) {
      claimAndVestRewards(_tokenIds[i]);
    }
  }

  function _createVestAndMint(address _user, uint256 _amount) internal {
    // store metadata for earned tokens in vesting contract for user who is unstaking
    vester.createVest(_user, _amount);
    // mint earned tokens to vesting contract
    CHI.stakeMintToVester(_amount);
  }

  // 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 * royaltyBasisPoints) / 1000);
  }

  function tokenURI(uint256 _tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(_exists(_tokenId), 'token does not exist');
    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'));
  }