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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../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.
 *
 * The initial owner is set to the address provided by the deployer. 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;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

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

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling 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 {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _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/token/ERC721/IERC721Receiver.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721Receiver.sol)

pragma solidity ^0.8.20;

/**
 * @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 `IERC721Receiver.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

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

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

// File: contracts/EtherStake.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {IERC721A} from"erc721a/contracts/IERC721A.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract EtherStake is IERC721Receiver, Ownable {

    error SenderIsNotStaker();
    event Stake(uint256 indexed tokenId, address indexed owner, uint256 when);
    event Unstake(uint256 indexed tokenId, address indexed owner, uint256 when);

    IERC721A public avatarContract;
    bool public avatarContractLocked;
    uint256 startingTime;

    mapping(address=>bool) public ownerWithdraw;
    mapping(uint256=>address) public staker;

    constructor(address _address, uint256 _startingTime) Ownable(msg.sender) {
        avatarContract = IERC721A(_address);

        startingTime = _startingTime;
    }

    /*
     * Internal function to handle the staker variable and Stake event emit.
     */

    function _stake(uint256 tokenId, address from) internal {
        staker[tokenId] = from;
        emit Stake(tokenId, from, block.timestamp);
    }

    /*

    ##############################################################################################
    #
    #
    # THESE FUNCTIONS ARE FOR USERS.
    #
    #
    ##############################################################################################

    */

    /**
     * Stakes the given NFTs to the contract and emits the Stake event. [REQUIRES APPROVAL OF THIS CONTRACT'S ADDRESS FOR EACH TOKEN OR ALL TOKENS ON AVATAR CONTRACT.]
     * 
     * This option is identical to the stakeSafeTransfer but uses transferFrom instead of safeTransferFrom.
     * transferFrom uses less gas than stakeSafeTransfer due to it avoiding running/checking onERC721Received which is correctly implemented on address(this).
     * The difference is pretty minimal in terms of gas cost, but I decided to give both options since I was going to implement onERC721Received either way to catch or block any accidental safeTransferFroms.
     *
     * stake() is preferred to be used over stakeSafeTransfer()
     * 
     * @param tokenIds Array of tokenIds that the sender owns and has approved to transfer to the staking contract.
     */

    function stake(uint256[] calldata tokenIds) external {
        require(block.timestamp > startingTime, 'Staking is not open.');
        
        for(uint256 i; i < tokenIds.length;){
            avatarContract.transferFrom(msg.sender, address(this), tokenIds[i]);
            _stake(tokenIds[i], msg.sender);
            unchecked{++i;}
        }
    }

    /**
     * Stakes the given NFTs to the contract and emits the Stake event. [REQUIRES APPROVAL OF THIS CONTRACT'S ADDRESS FOR EACH TOKEN OR ALL TOKENS ON AVATAR CONTRACT.]
     *
     * Read above for difference between stake() and stakeSafeTransfer()
     * 
     * @param tokenIds Array of tokenIds that the sender owns and has approved to transfer to the staking contract.
     */ 

    function stakeSafeTransfer(uint256[] calldata tokenIds) external {
        require(block.timestamp > startingTime, 'Staking is not open.');
        
        for(uint256 i; i < tokenIds.length;){
            avatarContract.safeTransferFrom(msg.sender, address(this), tokenIds[i]); // go to: onERC721Received
            unchecked{++i;}
        }
    }

    function onERC721Received(address, address from, uint256 tokenId, bytes calldata) external returns (bytes4) {
        require(block.timestamp > startingTime, 'Staking is not open.');
        require(msg.sender == address(avatarContract), 'Invalid receiving NFT contract address.');

        _stake(tokenId, from);
        return IERC721Receiver.onERC721Received.selector;
    }


    /**
     * Unstakes the given NFTs from this contract and emits.
     * 
     * @param tokenIds Array of tokenIds that the sender owns and wants to transfer out.
     */

    function unstake(uint256[] calldata tokenIds) external {
        for(uint256 i; i<tokenIds.length;){
            if (staker[tokenIds[i]] != msg.sender){ // Person unstaking must be the person who staked.
                revert SenderIsNotStaker();
            }

            delete staker[tokenIds[i]];
            emit Unstake(tokenIds[i], msg.sender, block.timestamp);

            avatarContract.safeTransferFrom(address(this), msg.sender, tokenIds[i]);

            unchecked{++i;}
        }
    }

    /*

    ##############################################################################################
    #
    #
    # THE BELOW FUNCTIONS ARE OWNER-ONLY OR ARE LIKELY NOT EVER NEEDING TO BE TOUCHED BY A USER.
    # AVOID INTERACTING WITH THE BELOW FUNCTIONS UNLESS YOU CONTACT SUPPORT FIRST TO VERIFY.
    #
    #
    ##############################################################################################

    */


    /**
     * Allows the owner to modify the avatar contract address before staking begins.
     *
     * @param _address is the Ether Avatar NFT address.
     */
    
    function updateAvatarAddress(address _address) external onlyOwner {
        require(!avatarContractLocked, 'Avatar contract cannot be changed now.');

        avatarContract = IERC721A(_address);
    }

    /**
     * Blocks the owner from modifying the avatar contract address. Cannot be undone.
     */
    
    function lockAvatarAddress() external onlyOwner {
        avatarContractLocked = true;
    }

    /**
     * Pushes back the starting time for staking if there is a last-minute delay. Cannot be delayed once staking has opened.
     *
     * @param _startingTime is the staking starting time.
     */

    function delay(uint256 _startingTime) external onlyOwner {
        require(startingTime > block.timestamp, 'Cannot delay staking once it has started.');

        startingTime = _startingTime;
    }

    /**
     * If someone accidentally locks another NFT / locks an avatar into the contract with a bad/unsafe transfer, the contract owner can choose to withdraw it out to a specific person.
     *
     * This function does NOT allow for properly staked avatars to be withdrawn by the contract owner unless given permission by the staker themselves. Properly staked avatars must be unstaked by the address who staked them in the first place unless the owner is allowed.
     *
     * @param contractAddress is the contract for the locked NFT.
     * @param tokenId is the tokenId for the locked NFT.
     * @param to is the person who should receive it.
     */

    function withdrawERC721(address contractAddress, uint256 tokenId, address to) external onlyOwner {
        // If the given contract address is the avatar contract,
        if(contractAddress == address(avatarContract)) {

            // If the avatar was either improperly staked OR staker allows owner withdrawals,
            require(staker[tokenId] == address(0) || ownerWithdraw[staker[tokenId]] == true, 'Avatar is properly staked.'); // Protects properly staked avatars and only allows withdraw if the staker has allowed the owner to withdraw.
            
            // If unstaking for a person who properly staked and allowed the contract owner to withdraw, emit the Unstake event and mark as unstaked to simulating unstake([tokenId]),
            if(staker[tokenId] != address(0)){
                emit Unstake(tokenId, staker[tokenId], block.timestamp);
                delete staker[tokenId];
            }

            // Then transfer it.
            avatarContract.safeTransferFrom(address(this), to, tokenId);

        }
        else{
            // Any non-avatar NFT can be transferred out without permissions required similar to an improperly staked avatar.
            IERC721A(contractAddress).safeTransferFrom(address(this), to, tokenId);
        }
    }

    /**
     * Allows the owner to withdraw any properly staked NFT YOU own and staked. [DO NOT RUN UNLESS YOU KNOW WHAT YOU'RE DOING AND TRUST CONTRACT OWNER. TOGGLE BACK TO FALSE AFTER THE OWNER HAS DONE WHAT YOU NEED.]
     *
     * ownerWithdraw is false by default. Running this function toggles the owner to be able to withdraw any of YOUR properly staked NFTs.
     *
     * This is for cases where:
     * - someone sends from an address that cannot receive new ERC721s (so unstake() will fail requiring assistance to unstake to a valid ERC721 receiver)
     * - a wallet is compromised, you can allow the owner to withdraw the nfts without them getting sent to the compromised wallet (less useful since an attacker could just unstake themselves if they know you've got Avatars staked.)
     */
    function toggleOwnerWithdraw() external {
        ownerWithdraw[msg.sender] = !ownerWithdraw[msg.sender]; // ALLOWS CONTRACT OWNER TO CONTROL YOUR PROPERLY STAKED AVATARS!
    }

}

// File: erc721a/contracts/IERC721A.sol



// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

/**
 * @dev Interface of ERC721A.
 */
interface IERC721A {
    /**
     * The caller must own the token or be an approved operator.
     */
    error ApprovalCallerNotOwnerNorApproved();

    /**
     * The token does not exist.
     */
    error ApprovalQueryForNonexistentToken();

    /**
     * Cannot query the balance for the zero address.
     */
    error BalanceQueryForZeroAddress();

    /**
     * Cannot mint to the zero address.
     */
    error MintToZeroAddress();

    /**
     * The quantity of tokens minted must be more than zero.
     */
    error MintZeroQuantity();

    /**
     * The token does not exist.
     */
    error OwnerQueryForNonexistentToken();

    /**
     * The caller must own the token or be an approved operator.
     */
    error TransferCallerNotOwnerNorApproved();

    /**
     * The token must be owned by `from`.
     */
    error TransferFromIncorrectOwner();

    /**
     * Cannot safely transfer to a contract that does not implement the
     * ERC721Receiver interface.
     */
    error TransferToNonERC721ReceiverImplementer();

    /**
     * Cannot transfer to the zero address.
     */
    error TransferToZeroAddress();

    /**
     * The token does not exist.
     */
    error URIQueryForNonexistentToken();

    /**
     * The `quantity` minted with ERC2309 exceeds the safety limit.
     */
    error MintERC2309QuantityExceedsLimit();

    /**
     * The `extraData` cannot be set on an unintialized ownership slot.
     */
    error OwnershipNotInitializedForExtraData();

    // =============================================================
    //                            STRUCTS
    // =============================================================

    struct TokenOwnership {
        // The address of the owner.
        address addr;
        // Stores the start time of ownership with minimal overhead for tokenomics.
        uint64 startTimestamp;
        // Whether the token has been burned.
        bool burned;
        // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}.
        uint24 extraData;
    }

    // =============================================================
    //                         TOKEN COUNTERS
    // =============================================================

    /**
     * @dev Returns the total number of tokens in existence.
     * Burned tokens will reduce the count.
     * To get the total number of tokens minted, please see {_totalMinted}.
     */
    function totalSupply() external view returns (uint256);

    // =============================================================
    //                            IERC165
    // =============================================================

    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);

    // =============================================================
    //                            IERC721
    // =============================================================

    /**
     * @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,
        bytes calldata data
    ) external payable;

    /**
     * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external payable;

    /**
     * @dev Transfers `tokenId` 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 payable;

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

    /**
     * @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 the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

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

    // =============================================================
    //                        IERC721Metadata
    // =============================================================

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

    // =============================================================
    //                           IERC2309
    // =============================================================

    /**
     * @dev Emitted when tokens in `fromTokenId` to `toTokenId`
     * (inclusive) is transferred from `from` to `to`, as defined in the
     * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard.
     *
     * See {_mintERC2309} for more details.
     */
    event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
}