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



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (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 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 {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @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 {
        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/security/Pausable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (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 Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

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

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

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        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/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

// 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: @openzeppelin/contracts/utils/math/Math.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol)

pragma solidity ^0.8.0;

/**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {
    enum Rounding {
        Down, // Toward negative infinity
        Up, // Toward infinity
        Zero // Toward zero
    }

    /**
     * @dev Returns the largest of two numbers.
     */
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }

    /**
     * @dev Returns the smallest of two numbers.
     */
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    /**
     * @dev Returns the average of two numbers. The result is rounded towards
     * zero.
     */
    function average(uint256 a, uint256 b) internal pure returns (uint256) {
        // (a + b) / 2 can overflow.
        return (a & b) + (a ^ b) / 2;
    }

    /**
     * @dev Returns the ceiling of the division of two numbers.
     *
     * This differs from standard division with `/` in that it rounds up instead
     * of rounding down.
     */
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        // (a + b - 1) / b can overflow on addition, so we distribute.
        return a == 0 ? 0 : (a - 1) / b + 1;
    }

    /**
     * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
     * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
     * with further edits by Uniswap Labs also under MIT license.
     */
    function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
            // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2^256 + prod0.
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly {
                let mm := mulmod(x, y, not(0))
                prod0 := mul(x, y)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division.
            if (prod1 == 0) {
                // Solidity will revert if denominator == 0, unlike the div opcode on its own.
                // The surrounding unchecked block does not change this fact.
                // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
                return prod0 / denominator;
            }

            // Make sure the result is less than 2^256. Also prevents denominator == 0.
            require(denominator > prod1, "Math: mulDiv overflow");

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0].
            uint256 remainder;
            assembly {
                // Compute remainder using mulmod.
                remainder := mulmod(x, y, denominator)

                // Subtract 256 bit number from 512 bit number.
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
            // See https://cs.stackexchange.com/q/138556/92363.

            // Does not overflow because the denominator cannot be zero at this stage in the function.
            uint256 twos = denominator & (~denominator + 1);
            assembly {
                // Divide denominator by twos.
                denominator := div(denominator, twos)

                // Divide [prod1 prod0] by twos.
                prod0 := div(prod0, twos)

                // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
                twos := add(div(sub(0, twos), twos), 1)
            }

            // Shift in bits from prod1 into prod0.
            prod0 |= prod1 * twos;

            // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
            // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
            // four bits. That is, denominator * inv = 1 mod 2^4.
            uint256 inverse = (3 * denominator) ^ 2;

            // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
            // in modular arithmetic, doubling the correct bits in each step.
            inverse *= 2 - denominator * inverse; // inverse mod 2^8
            inverse *= 2 - denominator * inverse; // inverse mod 2^16
            inverse *= 2 - denominator * inverse; // inverse mod 2^32
            inverse *= 2 - denominator * inverse; // inverse mod 2^64
            inverse *= 2 - denominator * inverse; // inverse mod 2^128
            inverse *= 2 - denominator * inverse; // inverse mod 2^256

            // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
            // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
            // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inverse;
            return result;
        }
    }

    /**
     * @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
     */
    function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
        uint256 result = mulDiv(x, y, denominator);
        if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
            result += 1;
        }
        return result;
    }

    /**
     * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
     *
     * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
     */
    function sqrt(uint256 a) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }

        // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
        //
        // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
        // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
        //
        // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
        // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
        // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
        //
        // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
        uint256 result = 1 << (log2(a) >> 1);

        // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
        // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
        // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
        // into the expected uint128 result.
        unchecked {
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            return min(result, a / result);
        }
    }

    /**
     * @notice Calculates sqrt(a), following the selected rounding direction.
     */
    function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = sqrt(a);
            return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 2, rounded down, of a positive value.
     * Returns 0 if given 0.
     */
    function log2(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >> 128 > 0) {
                value >>= 128;
                result += 128;
            }
            if (value >> 64 > 0) {
                value >>= 64;
                result += 64;
            }
            if (value >> 32 > 0) {
                value >>= 32;
                result += 32;
            }
            if (value >> 16 > 0) {
                value >>= 16;
                result += 16;
            }
            if (value >> 8 > 0) {
                value >>= 8;
                result += 8;
            }
            if (value >> 4 > 0) {
                value >>= 4;
                result += 4;
            }
            if (value >> 2 > 0) {
                value >>= 2;
                result += 2;
            }
            if (value >> 1 > 0) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 2, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log2(value);
            return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 10, rounded down, of a positive value.
     * Returns 0 if given 0.
     */
    function log10(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >= 10 ** 64) {
                value /= 10 ** 64;
                result += 64;
            }
            if (value >= 10 ** 32) {
                value /= 10 ** 32;
                result += 32;
            }
            if (value >= 10 ** 16) {
                value /= 10 ** 16;
                result += 16;
            }
            if (value >= 10 ** 8) {
                value /= 10 ** 8;
                result += 8;
            }
            if (value >= 10 ** 4) {
                value /= 10 ** 4;
                result += 4;
            }
            if (value >= 10 ** 2) {
                value /= 10 ** 2;
                result += 2;
            }
            if (value >= 10 ** 1) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 10, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log10(value);
            return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 256, rounded down, of a positive value.
     * Returns 0 if given 0.
     *
     * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
     */
    function log256(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >> 128 > 0) {
                value >>= 128;
                result += 16;
            }
            if (value >> 64 > 0) {
                value >>= 64;
                result += 8;
            }
            if (value >> 32 > 0) {
                value >>= 32;
                result += 4;
            }
            if (value >> 16 > 0) {
                value >>= 16;
                result += 2;
            }
            if (value >> 8 > 0) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 256, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log256(value);
            return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0);
        }
    }
}

// File: @openzeppelin/contracts/utils/math/SignedMath.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol)

pragma solidity ^0.8.0;

/**
 * @dev Standard signed math utilities missing in the Solidity language.
 */
library SignedMath {
    /**
     * @dev Returns the largest of two signed numbers.
     */
    function max(int256 a, int256 b) internal pure returns (int256) {
        return a > b ? a : b;
    }

    /**
     * @dev Returns the smallest of two signed numbers.
     */
    function min(int256 a, int256 b) internal pure returns (int256) {
        return a < b ? a : b;
    }

    /**
     * @dev Returns the average of two signed numbers without overflow.
     * The result is rounded towards zero.
     */
    function average(int256 a, int256 b) internal pure returns (int256) {
        // Formula from the book "Hacker's Delight"
        int256 x = (a & b) + ((a ^ b) >> 1);
        return x + (int256(uint256(x) >> 255) & (a ^ b));
    }

    /**
     * @dev Returns the absolute unsigned value of a signed value.
     */
    function abs(int256 n) internal pure returns (uint256) {
        unchecked {
            // must be unchecked in order to support `n = type(int256).min`
            return uint256(n >= 0 ? n : -n);
        }
    }
}

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



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

pragma solidity ^0.8.0;

import "./math/Math.sol";
import "./math/SignedMath.sol";

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant _SYMBOLS = "0123456789abcdef";
    uint8 private constant _ADDRESS_LENGTH = 20;

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        unchecked {
            uint256 length = Math.log10(value) + 1;
            string memory buffer = new string(length);
            uint256 ptr;
            /// @solidity memory-safe-assembly
            assembly {
                ptr := add(buffer, add(32, length))
            }
            while (true) {
                ptr--;
                /// @solidity memory-safe-assembly
                assembly {
                    mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
                }
                value /= 10;
                if (value == 0) break;
            }
            return buffer;
        }
    }

    /**
     * @dev Converts a `int256` to its ASCII `string` decimal representation.
     */
    function toString(int256 value) internal pure returns (string memory) {
        return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value))));
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        unchecked {
            return toHexString(value, Math.log256(value) + 1);
        }
    }

    /**
     * @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] = _SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
     */
    function toHexString(address addr) internal pure returns (string memory) {
        return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
    }

    /**
     * @dev Returns true if the two strings are equal.
     */
    function equal(string memory a, string memory b) internal pure returns (bool) {
        return keccak256(bytes(a)) == keccak256(bytes(b));
    }
}

// File: contracts/common/Signable.sol



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

import "@openzeppelin/contracts/access/Ownable.sol";

import "./../lib/Signature.sol";

// NOTE: Use distinct wallets for different admin roles/purposes, because it's safer.
// Role hierarchy:
// 1. operator (autohorized cold wallet, which should also be hardware and multi-signature (in this case it's actually a contract) wallet): `owner()`
// 2. automator (autohorized hot wallet, used from the automated backend): `defaultSigner`
// 3. sender (any wallet or contract): `_msgSender()`
abstract contract Signable is Ownable {

    // =======================================================================
    // Errors.
    // =======================================================================

    error ParamInvalid(uint8 paramPosIdx);

    error SenderInvalid();

    // =======================================================================
    // Events.
    // =======================================================================

    event DefaultSignerUpdated(address indexed oldSigner, address indexed newSigner);

    // =======================================================================
    // State vars.
    // =======================================================================

    // Automator.
    address public defaultSigner;
    // A count of number of TXs triggered from the sender, i.e. from his account/wallet/contract/address. Use and increment it in functions which do a state change and/or are payable, but are only needed if an outside user is the sender.
    // NOTE: Both smart contracts and wallets (i.e. EOA - Externally Owner Account) have nonces already implemented. However, contracts can't make transactions, they can only make calls that are triggered by transactions. But contracts still can create other contracts and for the newly generated contract the nonce of the sender is relevant. A nonce of a contract is only incremented when that contract creates another contract. There is no built-in method for a contract to access an account's nonce, including its own - hence, this contract uses its storage to keep track of the nonce for its senders, via this var. Note that via the frontend you can get those nonces via e.g. `web3.eth.getTransactionCount` funciton, but the nonce here is specifically for the TXs that were sent to this contract (by this contract's senders), so we need to use this one always for the senders, and can use the mentioned function call just for the contract's nonce, in our case.
    // NOTE: Since we use `uint24` type for the state vars which represent quantities per season (and its lower store nodes), e.g. the number of NFTs for the particular season (i.e. `Season.maxSupply` field; e.g. imagine 1 NFT per 2^24 = 16,8 million unique users for the particular season), we implicitly limit the size of the state vars, both dynamic arrays and mappings, which store user wallet and contract addresses doing state changing interactions with our contract (specifically, the ones successfully calling (i.e. without TX reverting) functions which update this state var), to `uint40` possible values altogether (i.e. across all seasons of all leagues and sports), and we don't limit the size to `uint32` possible values instead, because a user may use many wallets or contracts to interact with our contract, hence we add extra 8 bytes of possible values (i.e. 2^8 = 256 wallets and contracts per each NFT (ownable/transferable by the same user) from across all seasons' collections), which allows 2^40 / 2^24 = 256 * 256 = 65536 times more possible values. Hence the limit shouldn't be reachable.
    // NOTE: We limit the quantity of state changing interactions each user wallet or contract address interacting with our contract (specifically, the ones successfully calling (i.e. without TX reverting) functions which update this state var) can do altogether (i.e. across all seasons of all leagues and sports) to `uint24` possible values, i.e. 2^24 = 16,8 million. Hence the limit shouldn't be reachable.
    mapping(address => uint24) public senderNonces;

    // =======================================================================
    // Setter functions.
    // =======================================================================

    function setDefaultSigner(address _defaultSigner)
        external
        onlyOwner
    {
        // Validate input params.

        // NOTE: Should be an automator wallet, so it can prepare TXs automatically.
        if(_defaultSigner == address(0) || _defaultSigner == owner()) revert ParamInvalid(0);

        // Update/set state vars.

        address oldDefaultSigner = defaultSigner;
        defaultSigner = _defaultSigner;

        // Emit events.

        emit DefaultSignerUpdated(oldDefaultSigner, _defaultSigner);
    }

    // =======================================================================
    // Validator functions.
    // =======================================================================

    function _verifyNonOwnerCallSignature(address _signer, address _sender, bytes memory _data, bytes memory _signature, uint40 _signatureExpiryTimestamp)
        internal
        view
    {
        Signature.verifyNonOwnerCallSignature(_signer, _sender, senderNonces[_sender], _data, _signature,  _signatureExpiryTimestamp);
    }

}

// File: contracts/lib/Conversion.sol



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

library Conversion {

    // =======================================================================
    // Parser functions.
    // =======================================================================

    // This parses/interprets `bytes` type as a `string` type, and doesn't convert it instead. E.g. `bytes` value `0xa22cb465` becomes `string` value "0xa22cb465".
    // NOTE: Can be used when a signature is provided via both `bytes` and `string` type, becasuse verifying signed signature params works only if they are provided in the `string` format, instead of the `bytes` format. . Because we can't include the `bytes` type param in the signature, we verify separately that it's equivalent to its parsed `sting` type param/version.
    function parseBytesAsString(bytes memory _buffer)
        external
        pure
        returns(string memory)
    {
        // Fixed buffer size for hexadecimal conversion.
        bytes memory converted = new bytes(_buffer.length * 2);
        bytes memory base = "0123456789abcdef";

        // NOTE: For vars which represent (or iterate, in this case) buffers of arbitrary size, we use `uint256` type, even though maybe a smaller size could be enough, because when using elements that are smaller than 32 bytes, the smart contract's gas usage may be higher; this is because the EVM operates on (a word size of) 256 bits / 32 bytes at a time (to facilitate native hashing and elliptic curve operations), therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. Hence the limit shouldn't be reachable. Note, however, that we can optimize gas consumption when storing or loading statically-sized variables, because in state variables, structs and statically sized arrays (actually, this is true for all statically-sized variables, except for mappings and dynamically-sized arrays), e.g. `uint8` does cost less than a traditional `uint`, because of the tight variable packing feature (coding pattern; which is unfortunately, until the time of writing, not automatically achieved by the optimizer). Also be sure that in structs your uints are next to your other uints, and bytes next to bytes, etc., because this further increases the tightly packed features, because the order you declare your state vars can affect the overall number of storage slots your contract uses. If you declare 2 `uint128`'s and then a `uin256`, that will use 2 256-bit storage slots; 1 slot for the 2 `uint128`'s and 1 slot for the `uint256`. If you declare 1 `uint128`, then a `uint256`, then a `uint128`, your contract will use 3 storage slots, because it can't fit the middle `uint256` state variable into a storage slot with either of the `uint128`'s, so each variable will need to have its own storage slot. Note, by default, to put the dynamic types / (state) vars (i.e. `bytes`, dynamically-sized arrays and `mapping`s) at the end or the beginning, before other, statically-sized types / (state) vars; but this tactic might not always use less gas (if at all - not verified!) (if optimizer is enabled, it might use even more gas, but just for the initial deployment of contracts and libraries, of course). Note that the difference between the gas cost to deploy `unit8` and `uint256` is geeting smaller with newer versions of Solidity, e.g. in Solidity 0.8.10 the `unit8` is now only 100 gas more expensive than `uint256`, compared to the SOLC 0.4.18, where they cost 75414 and 73867 gas respectively, i.e. a difference of 1547 gas. Note also that using bigger types will make the signatures which use vars with such types significantly larger then they could have been, leading to tangible increase in the gas price of all the methods which deal with such signatures. See: https://fravoll.github.io/solidity-patterns/tight_variable_packing.html
        // NOTE: Related to the upper note, using smaller type values for storage hence makes sense, because writing to storage is expensive, and the compiler will pack multiple smaller arguments. For function parameters and memory variables, it only makes sense if you want to restrict the range of values: there's no packing, so it's neither more nor less expensive.
        // NOTE (TODO): Before deploying the contract to the testnet or mainnet, rearrange all storage vars in all contracts and libraries to optimize gas usage via tight variable packing feature.
        for (uint256 i = 0; i < _buffer.length; i++) {
            converted[i * 2] = base[uint8(_buffer[i]) / base.length];
            converted[i * 2 + 1] = base[uint8(_buffer[i]) % base.length];
        }

        return string(abi.encodePacked("0x", converted));
    }

}

// File: contracts/lib/Signature.sol



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

library Signature {

    // =======================================================================
    // Errors.
    // =======================================================================

    error SignatureInvalid();
    error SignatureExpired();

    // =======================================================================
    // Parser functions.
    // =======================================================================

    // The prefix is a way to distinguish signatures meant for Ethereum smart contracts, and for other cryptographic platforms. By adding the Ethereum-specific prefix, the message (with the prefix) results in a different signature, specific just for Ethereum. But if there were no platform-specific prefixes, the resulting signature would be the same for all platforms, which could potentially lead to signature replay attacks.
    // NOTE: Back in 2016, when this standard was introduced, there were no other EVM networks (e.g. Binance Smart Chain and Polygon), or at least they were not widely used. Nowdays it's common to use the "Ethereum" prefix even if the message is intended for other EVM networks such as Polygon.
    // NOTE: The specific specific byte `0x19` was standardized simply because an already existing implementation (in the Go Ethereum client software, i.e. Geth) was using it before the standard was finalized. And the last number (`32` in this case) is the byte length of the message (excluding the prefix) that is being signed.
    // NOTE: E.g. Geth client software prepends the string `\x19Ethereum Signed Message:\n32` to all data before signing it (https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign). If you want to verify such a signature from Solidity, you have to prepend the same string in Solidity before doing the `ecrecover`. The reason was that by allowing signing arbitrary data, a DApp can trick the user to sign a transaction masqueraded as some arbitrary application data. This prefix ensures that no matter what data a DApp sends to Geth, it cannot be abused as a transaction. To use this mechanism for signature verification in solidity, you'll need to have your contract also do this prefixing.
    function _getEthPrefixedHash(bytes32 _hash)
        private
        pure
        returns(bytes32)
    {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash));
    }

    function _splitSignature(bytes memory _signature)
        private
        pure
        returns(uint8 v, bytes32 r, bytes32 s)
    {
        // Validate input params.

        if (_signature.length != 65) revert SignatureInvalid();

        assembly {
            // First 32 bytes, after the length prefix.
            r := mload(add(_signature, 32))
            // Second 32 bytes.
            s := mload(add(_signature, 64))
            // Final byte (first byte of the next 32 bytes).
            v := byte(0, mload(add(_signature, 96)))
        }

        return (v, r, s);
    }

    function _recoverSigner(bytes32 _message, bytes memory _signature)
        private
        pure
        returns(address _signer)
    {
        (uint8 v, bytes32 r, bytes32 s) = _splitSignature(_signature);

        return ecrecover(_message, v, r, s);
    }

    // =======================================================================
    // Validator functions.
    // =======================================================================

    // Same as `verifyNonOwnerCallSignature` function, except that it's meant for the cases where the signer (e.g. operator) is also the sender, or the sender is controlled by or in the same trustworthy group as the signer (e.g. automator). In which case the meta params don't need to be verified, as the verification is already safe enough without them. It can also be used for any other type of signature verifications (e.g. for encrypting/encoding (actually jsut hashing) secret data, e.g. via Commit-Reveal Schemes).
    function verifySignature(address _signer, bytes memory _data, bytes memory _signature)
        public
        pure
    {
        bytes32 message = _getEthPrefixedHash(keccak256(_data));

        if (_recoverSigner(message, _signature) != _signer) revert SignatureInvalid();
    }

    // We need to use this signature verification inside functions which should only be executed if the signer (e.g. operator) confirmed (preparred) the TX to be sent/transmitted to the smart ocntract (by providing a signature param to the calling function), to prevent non-signer (i.e. sender) wallets or contracts from tempering the function params' values confirmed/provided by the signer.
    // NOTE: Include all calling function's params in the singature, even the ones that are not a potential threat if they are changed intentionally or unintentionally by the sender (even if the signer also owns the sender), just to make sure that the sender transmits exactly the same TX as we signed/confirmed, without being able to modify any of the params. The redundant params (e.g. `NftType` struct's excess params) can be omitted, to reduce hitting the vars limit (i.e. getting stack too deep error).
    // NOTE: We use `uint40` type for `_signatureExpiryTimestamp` param. Unix time is traditionally stored in a 32-bit number. This will lead to the "Year 2038" problem, when 32-bit Unix timestamps will overflow, because it won't be able to store that high number. To avoid this issue, we can choose to use 64-bit number instead (in Solidity 40-bit can be used and is enough), although that will require a higher gas fee.
    function verifyNonOwnerCallSignature(address _signer, address _sender, uint24 senderNonce, bytes memory _data, bytes memory _signature, uint40 _signatureExpiryTimestamp)
        external
        view
    {
        // Validate input params.

        // Optionally (i.e. if `_signatureExpiryTimestamp > 0`) validate the signature isn't expired yet (specifically, that it's not past, but can be the same as, the provided expiry timestamp).
        // NOTE: `block.timestamp` is a Unix timestamp of the latest block (the number of seconds that have passed since January 1st 1970).
        if (_signatureExpiryTimestamp > 0 && block.timestamp > _signatureExpiryTimestamp) revert SignatureExpired();

        _data = bytes.concat(
            // Meta params:
            // - Chain ID: To prevent replay attacks between different Ethereum compatible chains (e.g. between the main ETH and ETC chains, which both have a network ID of 1). Note that both network and chain ID are required for the chain to operate in general, e.g. it's required when signing transactions, meaning transactions signed on the ETH network end up with a different hash than those signed on ETC. Note that the "Ethereum" prefix added via `Signature._prefixed` function (see it) doesn't prevent replay attacks accross other EVM networks (e.g. Binance Smart Chain and Polygon), because they all use this same "Ethereum" prefix (when the "Ethereum" prefix standard was introduced, there were no other EVM networks, or at least they were not widely used), hence why we use this meta param.
            // - This contracts's address: To make sure the sender is not sending/transmitting a TX that was signed for another contract with the same function signature (i.e. the same combination of the function's name and the types of parameters it takes).
            // - Sender public address: To make sure the signed TX is sent/transmitted by the same sender for which it was signed.
            // - Current sender nonce: A count of number of TXs triggered from the sender, i.e. from his account/address/wallet/contract. To prevent replay attacks, i.e. make sure the sender can't execute the signed TX anymore if he already sent/transmitted another TX (or many of them, and of the same or different function signatures) in the mean time, because those TXs could have changed the current state of the contract (i.e. it's state vars), potentially making the signature non valid anymore if our (backend) pre-validation for producing the signature for this same type of TX would now fail, hence this TX should now fail/be reverted if sent/transmitted. It can also be used for specifying the order in which the TXs should be executed.
            // - Max valid timestamp: Verified later on. To make sure the signed TX is sent/transmitted by the sender before the specified timestamp, before the provided timestamp is greater (or even equal) to the current block's timestamp, i.e. `block.timestamp`.
            abi.encodePacked(block.chainid, this, _sender, senderNonce),
            _data
        );

        verifySignature(_signer, _data, _signature);
    }

}

// File: contracts/lib/StoreDeleter.sol



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

import "@openzeppelin/contracts/utils/Strings.sol";

import "./../lib/StoreGetter.sol";

// NOTE: All these functions should be called in functions which are called only by (i.e. limited to) or signature gated by the operator.
library StoreDeleter {

    // =======================================================================
    // Libraries attached to var types.
    // =======================================================================

    using Strings for string;

    // =======================================================================
    // Errors.
    // =======================================================================

    error NodeHasChildren();
    error LeafNodeAlreadyUsed(uint8 arrayPosIdx);

    error AssetsLockError();

    // =======================================================================
    // Events.
    // =======================================================================

    event NodeDeleted(StoreGetter.NftType nftType, uint8 nodeDepth);

    // =======================================================================
    // Deleter functions.
    // =======================================================================

    function deleteSport(StoreGetter.Store storage store, StoreGetter.NftType memory _nftType)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 1-st/sport node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 1);

        // Validate existence of objects associated with the input params.
        StoreGetter.Sport storage sport = StoreGetter._getSport(store, _nftType);

        // Validate assets (e.g. ETH amount or NFTs) not getting irrevocably locked in the contract.
        // NOTE: NOT NEEDED in the current logic, since there are no assets managed in a sport `struct`.

        // Validate sport's state vars.

        if (sport.hLeagueNames.length > 0) revert NodeHasChildren();

        // Delete sport's `mapping` state vars, because a deletion of a (sport) `struct` containing mappings will not delete (i.e. reset to their default values) the mappings. All containing struct fields of any other type will be deleted/reset automatically, and deletion also recurse into struct members that are structs themselves and deletes them as well.
        // NOTE: NOT NEEDED in the current logic, because if all the current validations in this function pass, then all the sport `struct`'s mappings are either yet unused/uninitialized or already deleted/reset.

        // Delete sport's parent nodes' state vars.

        // Delete Mapped Struct.
        delete store.sports[_nftType.sport];

        // Delete Mapped Struct's Index. Preserve array order.
        // NOTE: We implicitly limit the `Store.hSportNames` array size to `uint8` possible values.
        uint8 i = 0;
        for (; i < store.hSportNames.length;) {
            if (store.hSportNames[i].equal(_nftType.sport)) {
                break;
            }
            unchecked { i++; }
        }
        for (; i < store.hSportNames.length - 1;){
            store.hSportNames[i] = store.hSportNames[i + 1];
            unchecked { i++; }
        }
        store.hSportNames.pop();

        // Emit events.

        emit NodeDeleted(_nftType, 1);
    }

    function deleteLeague(StoreGetter.Store storage store, StoreGetter.NftType memory _nftType)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 2-nd/league node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 2);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Sport storage sport = StoreGetter._getSport(store, _nftType);
        StoreGetter.League storage league = StoreGetter._getLeague(store, _nftType);

        // Validate assets (e.g. ETH amount or NFTs) not getting irrevocably locked in the contract.
        // NOTE: NOT NEEDED in the current logic, since there are no assets managed in a league `struct`.

        // Validate league's state vars.

        if (league.hSeasonNames.length > 0) revert NodeHasChildren();

        // Delete league's `mapping` state vars, because a deletion of a (league) `struct` containing mappings will not delete (i.e. reset to their default values) the mappings. All containing struct fields of any other type will be deleted/reset automatically, and deletion also recurse into struct members that are structs themselves and deletes them as well.
        // NOTE: NOT NEEDED in the current logic, because if all the current validations in this function pass, then all the league `struct`'s mappings are either yet unused/uninitialized or already deleted/reset.

        // Delete league's parent nodes' state vars.

        // Delete Mapped Struct.
        delete sport.leagues[_nftType.league];

        // Delete Mapped Struct's Index. Preserve array order.
        // NOTE: We implicitly limit the `Sport.hLeagueNames` array size to `uint8` possible values.
        uint8 i = 0;
        for (; i < sport.hLeagueNames.length;) {
            if (sport.hLeagueNames[i].equal(_nftType.league)) {
                break;
            }
            unchecked { i++; }
        }
        for (; i < sport.hLeagueNames.length - 1;){
            sport.hLeagueNames[i] = sport.hLeagueNames[i + 1];
            unchecked { i++; }
        }
        sport.hLeagueNames.pop();

        // Emit events.

        emit NodeDeleted(_nftType, 2);
    }

    function deleteSeason(StoreGetter.Store storage store, StoreGetter.NftType memory _nftType)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.League storage league = StoreGetter._getLeague(store, _nftType);
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);

        // Validate assets (e.g. ETH amount or NFTs) not getting irrevocably locked in the contract.

        if (season.spentPrizePoolAmount < season.totalPrizePoolAmount) revert AssetsLockError();

        // Validate season's state vars.

        if (season.hSaleNames.length > 0) revert NodeHasChildren();

        // Delete season's `mapping` state vars, because a deletion of a (season) `struct` containing mappings will not delete (i.e. reset to their default values) the mappings. All containing struct fields of any other type will be deleted/reset automatically, and deletion also recurse into struct members that are structs themselves and deletes them as well.
        // NOTE: NOT NEEDED in the current logic, because if all the current validations in this function pass, then all the season `struct`'s mappings are either yet unused/uninitialized or already deleted/reset.

        // Delete season's parent nodes' state vars.

        // Delete Mapped Struct.
        delete league.seasons[_nftType.season];

        // Delete Mapped Struct's Index. Preserve array order.
        // NOTE: We implicitly limit the `League.hSeasonNames` array size to `uint8` possible values.
        uint8 i = 0;
        for (;  i < league.hSeasonNames.length;) {
            if (league.hSeasonNames[i].equal(_nftType.season)) {
                break;
            }
            unchecked { i++; }
        }
        for (; i < league.hSeasonNames.length - 1;) {
            league.hSeasonNames[i] = league.hSeasonNames[i + 1];
            unchecked { i++; }
        }
        league.hSeasonNames.pop();

        // Emit events.

        emit NodeDeleted(_nftType, 3);
    }

    function deleteSale(StoreGetter.Store storage store, StoreGetter.NftType memory _nftType)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 4-th/sale node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 4);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);
        StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);

        // Validate assets (e.g. ETH amount or NFTs) not getting irrevocably locked in the contract.
        // NOTE: NOT NEEDED in the current logic, since there are no assets managed in a sale `struct`.

        // Validate sale's state vars.

        // NOTE: We implicitly limit the `Sale.tiers` array size to `uint8` possible values via `NftType.tier` field.
        uint8 i = 0;
        for (; i < sale.tiers.length;) {
            // Validate tier's state vars.
            StoreGetter.Tier storage tier = sale.tiers[i];
            if (tier.sold > 0) revert LeafNodeAlreadyUsed(i);

            // Read excess old tier's state vars before they are deleted/reset. Update sale's parent nodes' state vars.
            // NOTE: Not needed, since we do this via `sale.hMaxSupply` field later on.
            // season.hTotalSalesMaxSupply -= tier.maxSupply;

            // Delete tier's state vars.
            // NOTE: NOT NEEDED, because the arrays (both static and dynamic) are deleted (i.e. reset to their default values) automatically later on, upon the deletion of the struct containing them, hence it's more efficient without doing this.
            // sale.tiers.pop();
            unchecked { i++; }
        }

        // Update sale's parent nodes' state vars.

        season.hTotalSalesMaxSupply -= sale.hMaxSupply;

        // Delete sale's `mapping` state vars, because a deletion of a (sale) `struct` containing mappings will not delete (i.e. reset to their default values) the mappings. All containing struct fields of any other type will be deleted/reset automatically, and deletion also recurse into struct members that are structs themselves and deletes them as well.
        // NOTE: NOT NEEDED in the current logic, because there are no mappings in the sale `struct`.

        // Delete sale's parent nodes' state vars.

        // Delete Mapped Struct.
        delete season.sales[_nftType.sale];

        // Delete Mapped Struct's Index. Preserve array order.
        // NOTE: We implicitly limit the `Season.hSaleNames` array size to `uint8` possible values.
        for (i = 0; i < season.hSaleNames.length;) {
            if (season.hSaleNames[i].equal(_nftType.sale)) {
                break;
            }
            unchecked { i++; }
        }
        for (; i < season.hSaleNames.length - 1;) {
            season.hSaleNames[i] = season.hSaleNames[i + 1];
            unchecked { i++; }
        }
        season.hSaleNames.pop();

        // Emit events.

        emit NodeDeleted(_nftType, 4);
    }

}

// File: contracts/lib/StoreGetter.sol



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

import "./../nfts/IAkshunSeasonPassNft.sol";

// NOTE: Declaring all of this library's functions' visibility to `external` doesn't make this library be deployed separately from the `AkshunStore.sol` smart contract, for some unknown reason. Note also that using `external` on any of the functions in this library would actually bloat both this library and `AkshunStore.sol` contract even more, for some reason, hence we use `internal` for them, although it's very counterintuitive, but we save on gas costs (both for deployment, but also when calling them, since now they are internally embedded in the contract using this library, because of the function(s) with the `internal` visibility).
library StoreGetter {

    // =======================================================================
    // Custom errors.
    // =======================================================================

    error NodeNameInvalid(uint8 nodeDepth);
    error NodeNonexistent(uint8 nodeDepth);

    // =======================================================================
    // State var definitions.
    // =======================================================================

    string constant publicSaleName = "Public";
    string constant customPrizeType = "Custom";

    // Hierarchical store structure.
    // NOTE: Using nested `Mapped Structs with Index` storage pattern.

    // What types to use:
    // - For state vars which represent quantities per season (and its lower store nodes), e.g. the number of NFTs for the particular season (i.e. `Season.maxSupply` field), we use `uint24` type, because 2^24 = 16,8 million possible values should be enough (e.g. imagine 1 NFT per 16,8 million unique users for the particular season).
    // - The same as for the state vars which represent quantities per season, goes also for the state vars which represent ETH amounts and prices per season (and its lower store nodes), i.e. using `uint24` type. But because the inherited contracts already use `uint256` (hence it wouldn't be compatibel with our amounts), and sent ETH amount `msg.value` also uses `uint256` (hence it wouldn't be compatibel with our prices/costs), we use that instead. Hence the limits shouldn't be reachable.
    // - Related to the previous two notes, the lower store nodes use types which are bigger than necessary, because the higher store nodes represent values altogether (i.e. across all lower store nodes). We don't use smaller types in such cases, so that we can use the same type for all of them (simplifies operations between them and their managements, and potentially reduces computational costs).
    // - For state vars which are dynamic arrays, we implicitly limit their size to `uint8` possible values, because 2^8 = 256 possible values should be enough for all the sports, all the sport's leagues, all the league's seasons, all the season's sales and all the sale's tiers, not altogether but separately (i.e. each of the mentioned groups has its own 256 possible values/slots; except for tiers, where there are 255 possible possible values/slots, because Tier grades start with 1, but their position indexes in `Tier.tiers` start with 0). The same goes for the mapping state vars which have such dynamic array dependent on it. Hence both limit shouldn't be reachable.
    // - Solidity timestamp uses Unix timestamp in seconds format (i.e. the number of seconds that have passed since January 1st 1970) and the type `uint256` is used by the timestamp vars in Solidity (e.g. `block.timestamp`), so use this type for timestamps. Unix time is traditionally stored in a 32-bit number. This will lead to the "Year 2038" problem, when 32-bit Unix timestamps will overflow, because it won't be able to store that high number. To avoid this issue, we can choose to use 64-bit number instead - in Solidity 40-bit can be used for the timestamps and is enough, although that will require a higher gas fee compared to 32-bit type.
    // - Related to the previous note, for delta time vars we use `uint24`, because cca. 16.8 minutes (= 11.651 days) should be enough for any delta time. Note the delta time shouldn't be too small in practice, since miners can vary or cheat the timestamp of the block by some tolerance. In practice, there has not been any notable timestamp skew ever. The block time itself is ~15 seconds, so as a rule of thumb it would be accurate to a minute. The only rule about timestamps is that the timestamp of a block must be greater than the previous block's timestamp; there is no other rule - old docs such as the white paper and wiki may mention 15 minutes (900 seconds), but these were corrected; the Yellow paper is the formal specification (see Block Header Validity (section 4.4.4, equation 48)). Small differences in time are tolerated by nodes, but as one node's time gets further away from the Coordinated UTC Time (per Network Time Protocol (NTP)), its number of peers will reduce and eventually it will have zero peers and be disconnected from the network. A miner wants to have a time consistent with the network, so that other miners will build upon the blocks that this miner mines. Blocks must be within a reasonable Unix time, otherwise miners are unlikely to build upon blocks with unreasonable timestamps.

    // NOTE: `Store` struct is needed to encompass the top-level vars. This way we can pass all the vars (fields) to other library and other contract functions via a single `Store` reference.
    // NOTE: The struct definition could also be put in a separate library, e.g. `StoreStructure`, and imported here. Then this library's current name `StoreGetter` would become more appropriate.
    struct Store {
        // NOTE: Since we use `uint24` type for the state vars which represent ETH amounts per season (and its lower store nodes), which allows 2^32 / 2^24 = 256 times more possible values. But because the inherited contracts already use `uint256` (hence it wouldn't be compatibel with our amounts), and sent ETH amount `msg.value` also uses `uint256` (hence it wouldn't be compatibel with our prices/costs), we use that instead. Hence the limit shouldn't be reachable.
        uint256 unspentOperationalFundAmount;

        // NOTE: We implicitly limit the array size to `uint16` possible values via `AkshunSeasonPassNft.hTokenSubcollectionIdx` var.
        // NOTE: Each season equals a unique subcollection.
        uint16 subcollectionsCnt;

        // TODO: Make it season bound!
        // Whether automator (`defaultSigner`) can sign free mints, otherwise only operator (`operator`) can. Less safe, but automation is not hindered.
         bool canAutomatorSignFreeMints;

        // Sports.

        // NOTE: We implicitly limit the mapping size to `uint8` possible values via `Store.hSportNames` field.
        mapping(string => Sport) sports;
        // NOTE: We implicitly limit the array size to `uint8` possible values.
        string[] hSportNames; // NOTE: Helper field practically underivable (because of `mapping` type) from `sports` (i.e. a list of `sports` Mapped Struct's Indexes), needed because you can't get them (or their existance) neatly and efficiently from the `Store.sports` mapping (via its keys).
    }

    struct Sport {
        bool hExists; // NOTE: Helper field derivable from `Store.hSportNames`, explicitly indicating whether the (sport) struct instance (i.e. node, fetched from `sports` mapping) exists, because it doesn't contain any other field which would have a non-default (i.e. non-reset) value after all of its nested structs (i.e. child nodes) are deleted, indicating an existing struct.

        // Types of prizes and the corresponding ETH prizes (which are specified as a % of the NFT type's minting prize) which can be claimed.
        // NOTE: We implicitly limit the array size to `uint8` possible values.
        string[] prizeTypes;
        mapping(string => bool) hPrizeTypesExist; // NOTE: Helper field derivable from `Sport.prizeTypes`, explicitly indicating whether a particular prize type exists, to assure unique prize types in `prizeTypes` field.

        // Leagues.

        // NOTE: We implicitly limit the mapping size to `uint8` possible values via `Sport.hLeagueNames` field.
        mapping(string => League) leagues;
        // NOTE: We implicitly limit the array size to `uint8` possible values.
        string[] hLeagueNames; // NOTE: Helper field practically underivable (because of `mapping` type) from `leagues` (i.e. a list of `leagues` Mapped Struct's Indexes), needed because you can't get them (or their existance) neatly and efficiently from the `Sport.leagues` mapping (via its keys).
    }

    struct League {
        bool hExists; // NOTE: Helper field derivable from `Sport.hLeagueNames`, explicitly indicating whether the (league) struct instance (i.e. node, fetched from `Sport.leagues` mapping) exists, because it doesn't contain any other field which would have a non-default (i.e. non-reset) value after all of its nested structs (i.e. child nodes) are deleted, indicating an existing struct.

        // Seasons.

        // NOTE: We implicitly limit the mapping size to `uint8` possible values via `League.hSeasonNames` field.
        mapping(string => Season) seasons;
        // NOTE: We implicitly limit the array size to `uint8` possible values.
        string[] hSeasonNames; // NOTE: Helper field practically underivable (because of `mapping` type) from `seasons` (i.e. a list of `seasons` Mapped Struct's Indexes), needed because you can't get them (or their existance) neatly and efficiently from the `League.seasons` mapping (via its keys).
    }

    // Each Season Pass NFT sub-collection is tied to a particular season.
    struct Season {
        // Dependent contract and library vars.

        // NOTE: Albeit they can use the same wallet, it might be safer having separate wallets for the Store contract owner and Season Pass NFT contract owner.
        // NOTE: The same NFT contract can be used across multiple seasons/subcollections (even of other leagues and sports), or a new one.
        IAkshunSeasonPassNft nftContract;
        // NOTE: We implicitly limit the array size to `uint16` possible values via `AkshunSeasonPassNft.hTokenSubcollectionIdx` var.
        uint16 subcollectionIdx;
        // The NFT's baseURI used when minting the whole season collection. The swaps are done with newer base URIs, preferably in batches (i.e. using the same new NFT's baseURI for multiple (potentially) swapped NFTS).
        // NOTE: Check it's dependent state var `tokenBaseURIIndexes` in `nftContract`/`AkshunSeasonPassNft.sol` smart contract.
        // NOTE: NOT NEEDED, since it's always `0` for the original minted (i.e. not burned and swapped) season's collection of Season pass NFTs.
        // uint8 initialTokenBaseURIIdx;

        // Commitment/Commit-Reveal Scheme to commit to a disposable wallet by storing its encrypted (specifically, by digitally signing it) public address in `encryptedRngProvider` (signature), and after the info doesn't need to be secret anymore (either after the whole season collection is minted out or the season is over, even if we still allow minting), we also store the unencrypted/revealed public address (revealedRngProvider), proving it's the same address which was before encrypted and also letting others see it. After a disposable wallet's hash is set, the wallet will be used to call a VRF (Verifiable Random Function), e.g. Chainlink VRF, to generate a provably fair and verifiable random number, without compromising security or usability. We commit to using a specific wallet before doing the call to the RNG, in order to prove we couldn't have manipulated it. The wallet is hashed so no one can see which RNG was created. This RNG will be used as a seed to determine the mapping between the NFTs and their metadata, providing a fully randomized distribution of NFTs.
        bytes encryptedRngProvider;
        address revealedRngProvider;

        // Overall mint quantities and amounts. Encompasses/includes all other mint quantities (e.g. free mint quantities).

        uint24 maxSupply; // NOTE: Also use this field as a field implicitly indicating whether the (season) struct instance (i.e. node, fetched from `League.seasons` mapping) exists.
        // A sum of max supplies of all season sales. I.e. the part of season's total supply which is already distributed among its sales' tiers.
        uint24 hTotalSalesMaxSupply; // NOTE: Helper field derivable from all season sales' `Tier.maxSupply`s. Must be `<= maxSupply`.
        uint24 hSold; // NOTE: Helper field derivable from all season sales' `Tier.sold`s. Must be `<= maxSupply`.

        // Free mint quantities. These quantities are accounted for in the overall mint quantities.

        uint24 maxFreeSupply; // NOTE: Must be `<= maxSupply`.
        uint24 freeSold; // NOTE: Must be `<= hSold`.

        // Delta time in minutes, starting from the provided timestamp of the creation of the digital signature (for the mint or swap function), until which the (signed) mint (or swap) TX can be executed. If it's not executed within this delta time, the signature verfication and hence the TX will revert/fail, and when we stop signing the TX for minting (or swapping), we need to wait for this delta time duration and then we can be sure no one can no longer execute any of the previously signed TXs (i.e. to do the next Season Pass NFTs batch reveal without breaking the IPFS metadata binding logic). This also prevents user from getting a lot of signed mint (or swap) TXs (with different wallets, because otherwise the nonce check would prevent executing more than one among the singed but not yet executed TXs) and executing them at once at a later time.
        uint24 mintPeriodMins;

        // Prize pool amounts.

        // BPS of the whole deposited ETH amount via Season Pass NFT mints/purchases reserved solely for the prize payouts.
        uint16 prizePoolBps; // NOTE: Must be `<= 10000`. BPS (or BP or BIPS) is a Basis Point(s), which equals one hundredth of 1 percentage point. I.e. 1% is 100 bps.
        // BPS of the ETH reserved solely for the prize payouts which can currently be distributed. A safety mechanism to prevent the season's whole prize pool drainage if the automator wallet gets compromised. A sensible BPS is between 5% and 10% higher than the current already distributed BPS of the whole prize pool amount.
        uint16 unlockedPrizePoolBps; // NOTE: Must be `<= 10000`. BPS (or BP or BIPS) is a Basis Point(s), which equals one hundredth of 1 percentage point. I.e. 1% is 100 bps.

        // Total ETH ever deposited (and/or moved from the operational funds) to the prize pool.
        uint256 totalPrizePoolAmount; // NOTE: We can't have a single e.g. `unspentPrizePoolAmount` field instead of both `totalPrizePoolAmount` and `spentPrizePoolAmount` fields, because this field is needed by `unlockedPrizePoolBps` field.
        uint256 spentPrizePoolAmount; // NOTE: Must be `<= totalPrizePoolAmount`.

        // Percentage of any of the Public sale/stage tiers' price for each sport's prize type in the dependent `Sport.prizeTypes`, used for calculating the prize pool payouts. Note that the winning Season Pass NFT's tier is used, but from the Public sale/stage instead.
        // NOTE: We implicitly limit the array size to `uint8` possible values, the same as we do for the dependent `Sport.prizeTypes` array.
        uint16[] prizeTypePcts; // NOTE: Must have the same size as `Sport.prizeTypes` array.
        // Whether a particular prize type was alredy used (paid out), hence its value in `prizeTypePcts` shouldn't change anymore.
        // NOTE: We could use bool array instead, but for existance/bool values it's more gas efficient and less error prone to use a mapping instead (because you need to push (e.g. initialize new elements as they come) and manage array elements).
        // NOTE: We limit the mapping value and implicitly limit the mapping size to `uint8` possible values, the same as we do for the dependent `Sport.prizeTypes` array.
        mapping(uint8 => bool) prizeTypesUsed;

        // Delta time in minutes, starting from the provided timestamp of the action associated with the claimable prize, until which the prize can be claimed. If it's not claimed within this delta time, the signature verfication and hence the TX will revert/fail, and the prize amount is again available for other prizes instead, as part of the total prize pool.
        uint24 prizeClaimPeriodMins;

        // Sales.

        // NOTE: We implicitly limit the mapping size to `uint8` possible values via `Season.hSaleNames` field.
        mapping (string => Sale) sales;
        // NOTE: We implicitly limit the array size to `uint8` possible values.
        string[] hSaleNames; // NOTE: Helper field practically underivable (because of `mapping` type) from `sales` (i.e. a list of `sales` Mapped Struct's Indexes), needed because you can't get them (or their existance) neatly and efficiently from the `Season.sales` mapping (via its keys).
    }

    struct Sale {
        bool hExists; // NOTE: Helper field derivable from `Season.hSaleNames`, explicitly indicating whether the (sale) struct instance (i.e. node, fetched from `Season.sales` mapping) exists, because it doesn't contain any other field which would have a non-default (i.e. non-reset) value after all of its nested structs (i.e. child nodes) are deleted, indicating an existing struct.

        // Whether the sale is meant for trying out, e.g. playing the game, but with much smaller/nominal tier prices and lesser prizes.
        bool tryout;

        // Mint quantities.

        // A sum of max supplies of all sale's tiers.
        uint24 hMaxSupply; // NOTE: Helper field derivable from all sale's `Tier.maxSupply`s. Total season sales' `Tier.maxSupply`s must be `<= Season.maxSupply`.
        uint24 hSold; // NOTE: Helper field derivable from all sale's `Tier.sold`s. Must be `<= hMaxSupply`.

        // Per wallet mint quantities.

        uint24 maxPerWalletSupply; // NOTE: Must be `<= hMaxSupply`.
        mapping(address => uint24) perWalletSold; // NOTE: Must be `<= maxPerWalletSupply`.
        uint24 hMaxPerWalletSold; // NOTE: Helper field almost impossibly derivable from `perWalletSold`.

        // Tiers.

        // Tier grade equals its `Tier[]` array's index plus 1.
        // NOTE: We implicitly limit the array size to `uint8` possible values via `NftType.tier` field.
        Tier[] tiers; // NOTE: We could use the `mapping` instead, but this enforces Tier grade order by design, albeit tier deletion becomes less gas-efficient. Also use this field as a field implicitly indicating whether a tier struct instance (i.e. node, yet to be fetched from `Sale.tiers` array) exists.
        // Tier for each minted Season Pass NFT, needed for computing the prizes won by it. Note that these are tiers (i.e. starting from 1), not their respective indexes (i.e. starting from 0).
        // NOTE: We implicitly limit the mapping value to `uint8` possible values via `NftType.tier` field.
        // NOTE: We use tier grades (i.e. their values start from 1), instead of tier indexes (i.e. their values start from 0), because then we can check whether the value is `0`, meaning the NFT/token with the provided ID doesn't exist (at least within the season), and also that the provided function param was specified.
        mapping(uint256 => uint8) tokenTiers; // NOTE: Values must be `> 0` and `<= tiers.length`. Also use this field as a field implicitly indicating whether the Season Pass NFT exists for this sale.
    }

    struct Tier {
        // Dependent contract and library vars.

        // NOTE: We implicitly limit the array size to `uint8` possible values via `AkshunSeasonPassNft.Subcollection.presetURIs` var.
        uint8 tokenPresetURIIdx;

        // Mint quantities.

        uint24 maxSupply; // NOTE: Sum of all parent sale's `Tier.tiersMaxSupply`s must be `<= Season.maxSupply`.
        // Amount of `Tier.maxSupply` which can currently be minted. A mechanism to enable gradual Season Pass NFT minting of the season sale's tier supply.
        uint24 unlockedMaxSupply; // NOTE: Must be `>= sold` and `<= maxSupply`.
        uint24 sold; // NOTE: Must be `<= tiersMaxSupply`.

        // Price amounts.

        uint256 price;
    }

    // Helper struct for passing NFT type params via function params.
    // NOTE: The bottom fields can have a default value (i.e. mimicking undefined values) when applicable (e.g. in function `getSeason` we don't need to specify the sale and tier). Beneficial for reducing the number of vars in a function. Use this as a function param whenever any of its encompassing fields are needed, to be consistent.
    struct NftType {
        string sport;
        string league;
        string season;
        string sale;
        // Tier grades start with 1, but their position indexes in `Tier.tiers` start with 0.
        // NOTE: We explicitly limit the array size to `uint8` possible values.
        uint8 tier;
    }

    // =======================================================================
    // Validator functions.
    // =======================================================================

    // NOTE: A nice side effect of using this function is preventing the usage of `NftType` struct's excess params, otherwise the events which emit `NftType` struct could emit different `NftType` struct values for the same NFT type, breaking the event filtering logic when filtering by the `NftType` struct (because all of struct's fields need to be provided, even if their default values are set for the fields which are not relevant in a particular use case, e.g. `NftType.sale` and `NftType.tier` fields when referring to a season node). But if the calling function is not changing the `StoreNodes` structure (i.e. state vars for it), don't use it (hence sacrifice events emitting `NftType` struct's excess params, but reducing gas costs), since using node getter functions (e.g. `_getSport`) is enough, because they will throw an error if the fetched node doesn't exist.
    //
    // NOTE: When using this function, we can omit `NftType` struct's excess params in the signature, if we're using one, but a cheaper alternative (not sure if true, because the signature computation rises because of the extra params) is to include them in the signature and skip using this function, but (probably) only if the calling function is already using/verifying a signature.
    function _validateNftType(NftType memory _nftType, uint8 _nodeDepth)
        internal
        pure
    {
        // Validate input params.

        if (bytes(_nftType.sport).length == 0) revert NodeNameInvalid(1);
        if (_nodeDepth >= 2 ? bytes(_nftType.league).length == 0 : bytes(_nftType.league).length > 0) revert NodeNameInvalid(2);
        if (_nodeDepth >= 3 ? bytes(_nftType.season).length == 0 : bytes(_nftType.season).length > 0) revert NodeNameInvalid(3);
        if (_nodeDepth >= 4 ? bytes(_nftType.sale).length == 0 : bytes(_nftType.sale).length > 0) revert NodeNameInvalid(4);
        if (_nodeDepth == 5 ? _nftType.tier == 0 : _nftType.tier > 0) revert NodeNameInvalid(5);
    }

    function _packNftTypeFields(StoreGetter.NftType memory _nftType)
        internal
        pure
        returns(bytes memory)
    {
        return abi.encodePacked(_nftType.sport, _nftType.league, _nftType.season, _nftType.sale, _nftType.tier);
    }

    // =======================================================================
    // Getter functions.
    // =======================================================================
    // NOTE: In the contract using this library, we could just get the particualr nodes, or even child store nodes from the parent ones directly without these getters, but they also perform the validation and reduce gas costs, in both of the mentioned cases. Hence, always use these getters to get the store nodes, even when getting a hierarchically connected nodes.
    // NOTE: Putting getters in a separate library, just as we did for deleters, costs us more in gas, since both the contract and the library get more bloated (at least for the deployment), hence we don't do it.

    function _getSport(Store storage _store, NftType memory _nftType)
        internal
        view
        returns(Sport storage)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        Sport storage sport = _store.sports[_nftType.sport];
        if (!sport.hExists) revert NodeNonexistent(1);

        // Get state vars.

        return sport;
    }

    function _getLeague(Store storage _store, NftType memory _nftType)
        internal
        view
        returns(League storage)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        League storage league = _store.sports[_nftType.sport].leagues[_nftType.league];
        if (!league.hExists) revert NodeNonexistent(2);

        // Get state vars.

        return league;
    }

    // NOTE: Types (i.e. `struct`) containing (nested or not) mappings can only be parameters or return variables of internal or library functions.
    function _getSeason(Store storage _store, NftType memory _nftType)
        internal
        view
        returns(Season storage)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        Season storage season = _store.sports[_nftType.sport].leagues[_nftType.league].seasons[_nftType.season];
        if (season.maxSupply == 0) revert NodeNonexistent(3);

        // Get state vars.

        return season;
    }

    function _getSale(Store storage _store, NftType memory _nftType)
        internal
        view
        returns(Sale storage)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        Sale storage sale = _store.sports[_nftType.sport].leagues[_nftType.league].seasons[_nftType.season].sales[_nftType.sale];
        if (!sale.hExists) revert NodeNonexistent(4);

        // Get state vars.

        return sale;
    }

    function _getTier(Store storage _store, NftType memory _nftType)
        internal
        view
        returns(Tier storage)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        // NOTE: If invalid `0` is supplied, the TX will be reverted with `panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)`.
        uint8 tierIdx = _nftType.tier - 1;
        Tier[] storage tiers = _store.sports[_nftType.sport].leagues[_nftType.league].seasons[_nftType.season].sales[_nftType.sale].tiers;
        if (tierIdx >= tiers.length) revert NodeNonexistent(5);

        // Get state vars.

        return tiers[tierIdx];
    }

}

// File: contracts/lib/StoreManager.sol



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

import "./../lib/StoreRevealCommit.sol";

// NOTE: All these functions should be called in functions which are called only by (i.e. limited to) or signature gated by the operator.
// NOTE: You can have `storage` data location for `external` functions' parameters in libraries, but not in contracts, because libraries don't have their own storage, they us theirs to load in the storage from the calling contract's storage and effectively operate and even change it. Doing the same in a contract would result in an error - Data location must be "memory" or "calldata" for parameter in external function, but "storage" was given. You could use the `.call()` function on the contract, but it's less efficient (also more gas costly) and less secure, and the data needs to be copied to the calling contract's function (instead of just being referenced, as is the case in a library). Hence we made a library, instead of a contract, for this code.
library StoreManager {

    // =======================================================================
    // Libraries attached to var types.
    // =======================================================================

    using Strings for string;

    // =======================================================================
    // Errors.
    // =======================================================================

    error ArrayParamInvalid(uint8 paramPosIdx, uint256 arrayPosIdx);

    error EthBalanceInsufficient();
    error EthWithdrawalFailed();

    // =======================================================================
    // Events.
    // =======================================================================

    event PrizesSetOrUpdated(StoreGetter.NftType nftType, string[] prizeTypes, uint16[] prizeTypePcts);

    event OperationalFundsMovedToPrizePool(StoreGetter.NftType nftType, uint256 amount);
    event OperationalFundsWithdrawn(uint256 amount, address indexed recipient);

    // =======================================================================
    // Setter functions.
    // =======================================================================

    // Secrets management.

    function commitRngProvider(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, bytes memory _encryptedRngProvider, string memory _encryptedRngProviderString)
        external
    {
        // Commit the encrypted (more precisely, signed) version of the secret data.
        StoreRevealCommit.commitRngProvider(_store, _nftType, _encryptedRngProvider, _encryptedRngProviderString);
    }

    function revealRngProvider(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, address _revealedRngProvider, address _signer)
        external
    {
        // Verify the previously committed encrypted (more precisely, signed) version of the secret data matches the unencrypted/unsigned/revealed one.
        StoreRevealCommit.revealRngProvider(_store, _nftType, _revealedRngProvider, _signer);
    }

    // Minting & burning management.

    function updateTierMaxSupplyLock(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, uint24 _unlockedMaxSupply)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 5-th/tier node) is done later on.
        // StoreGetter._validateNftType(_nftType, 5);

        // Validate existence of objects associated with the input params.
        StoreGetter.Tier storage tier = StoreGetter._getTier(_store, _nftType);

        if (_unlockedMaxSupply < tier.sold || _unlockedMaxSupply > tier.maxSupply) revert StoreRevealCommit.ParamInvalid(1);

        // Update season's state vars.

        tier.unlockedMaxSupply = _unlockedMaxSupply;

        // NOTE: No event emitted, because it's a part of a purily operational security management which shouldn't have any direct effects on the users.
    }

    // NOTE: Should be enabled only temporarily for a particular free mint batch to be minted, for security reasons.
    function updateCanAutomatorSignFreeMints(StoreGetter.Store storage _store, bool _canAutomatorSignFreeMints)
        external
    {
        // Update season's state vars.

        _store.canAutomatorSignFreeMints = _canAutomatorSignFreeMints;

        // NOTE: No event emitted, because it's a part of a purily operational security management which shouldn't have any direct effects on the users. Although it can be useful for automator to read this event, in order to be notified when it can free mint, it's a security concern, since this would also notify any other bad actors listening to it, tempting them to get an unauthorized access to the automator's wallet.
    }

    // Prizes management.

    function addPrizeTypes(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, string[] memory _prizeTypes)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 1-st/sport node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 1);

        // Validate existence of objects associated with the input params.
        StoreGetter.Sport storage sport = StoreGetter._getSport(_store, _nftType);

        // NOTE: The first `Sport.prizeTypes` is always a custom prize with a custom (not predefined) amount.
        if (_prizeTypes.length == 0 || sport.prizeTypes.length == 0 && !_prizeTypes[0].equal(StoreGetter.customPrizeType)) revert StoreRevealCommit.ParamInvalid(1);

        // Validate and update/set sport's state vars.

        // NOTE: `_prizeTypes` is a dynamic/unlimited array, but we implicitly limit it to `uint8` possible values and for the iterator we use the same `uint8` type as it is implicitly used for the dependent `Sport.prizeTypes` field size.
        for (uint8 i = 0; i < _prizeTypes.length;) {
            // Validate old/new sport's state vars.

            string memory prizeType = _prizeTypes[i];

            if (bytes(prizeType).length == 0 || sport.hPrizeTypesExist[prizeType]) revert ArrayParamInvalid(1, i);

            // Update/set sport's state vars.

            sport.prizeTypes.push(prizeType);
            sport.hPrizeTypesExist[prizeType] = true;
            unchecked { i++; }
        }

        // NOTE: No event emitted, because until the `Season.prizeTypePcts` field is also updated to set the corresponding values for the newly added `_prizeTypes`, this function has no effect on the business logic.
    }

    function setOrUpdatePrizes(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, uint24 _prizeClaimPeriodMins, uint16[] memory _prizeTypePcts)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Sport storage sport = StoreGetter._getSport(_store, _nftType);
        StoreGetter.Season storage season = StoreGetter._getSeason(_store, _nftType);

        if (_prizeClaimPeriodMins < season.prizeClaimPeriodMins) revert StoreRevealCommit.ParamInvalid(1);
        if (_prizeTypePcts.length != sport.prizeTypes.length) revert StoreRevealCommit.ParamInvalid(2);

        // Validate old/new season's state vars.

        // NOTE: `_prizeTypePcts` is a dynamic/unlimited array, but we implicitly limit it to `uint8` possible values and for the iterator we use the same `uint8` type as it is implicitly used for the dependent `Season.prizeTypePcts` field size.
        for (uint8 i = 0; i < _prizeTypePcts.length;) {
            // NOTE: `0` just means the prize type's percentage wasn't set yet.
            uint16 oldPrizeTypePct = i < season.prizeTypePcts.length ? season.prizeTypePcts[i] : 0;

            // NOTE: The first `Sport.prizeTypes` is always a custom prize with a custom (not predefined) amount.
            // NOTE: The prize types' percentages are independent from each other.
            if ((i == 0 && _prizeTypePcts[i] > 0) || season.prizeTypesUsed[i] && _prizeTypePcts[i] != oldPrizeTypePct || _prizeTypePcts[i] < oldPrizeTypePct) revert ArrayParamInvalid(2, i);

            // Update old prize type state vars.
            // NOTE: NOT NEEDED, because we assign the newly provided array to the one in the state variable later on, hence it's more efficient without doing this.
            // if (oldPrizeTypePct == 0) {
            //     season.prizeTypePcts[i] = _prizeTypePcts[i];
            // }
            unchecked { i++; }
        }

        // Update/set season's state vars.

        season.prizeTypePcts = _prizeTypePcts;
        season.prizeClaimPeriodMins = _prizeClaimPeriodMins;

        // Emit events.

        emit PrizesSetOrUpdated(_nftType, sport.prizeTypes, season.prizeTypePcts);
    }

    function updatePrizePoolLock(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, uint16 _unlockedPrizePoolBps)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(_store, _nftType);

        // NOTE: Division must always be the last operation, because decimals are truncated, since we are computing with integers.
        if (_unlockedPrizePoolBps > 10_000 || _unlockedPrizePoolBps <= season.spentPrizePoolAmount * 1e4 / season.totalPrizePoolAmount) revert StoreRevealCommit.ParamInvalid(1);

        // Update/set season's state vars.

        season.unlockedPrizePoolBps = _unlockedPrizePoolBps;

        // NOTE: No event emitted, because it's a part of a purily operational security management which shouldn't have any direct effects on the users, specifically, the prize pool should be unlocked further each time and before the `season.spentPrizePoolAmount` comes close to the unlocked amount defined by `season.unlockedPrizePoolBps`.
    }

    // Operational funds management.

    function receiveOperationalFunds(StoreGetter.Store storage _store, uint256 _amount)
        external
    {
        // NOTE: There are no side effects if the sent amount is `0`; adding validation to prevent/revert such case would be less gas efficient.
        _store.unspentOperationalFundAmount += _amount;
    }

    function moveOperationalFundsToPrizePool(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, uint256 _amount)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(_store, _nftType);

        if (_amount == 0) revert StoreRevealCommit.ParamInvalid(1);

        // Validate signer's signature.
        // NOTE: We don't use signature here, because it can place unnecessary overhead on the operations, i.e. it adds the need of creating a streamline/UI interface/API for getting specific signatures for this function. Hence it's advisable to use an alternative, which is using a multisignature wallet, such as Gnosis.

        // bytes memory data = abi.encodePacked(_nftType.sport, _nftType.league, _nftType.season, _amount);
        // Signature.verifySignature(_signer, data, _signature);

        // Validate and update operational fund's amount state var.

        if (_amount > _store.unspentOperationalFundAmount) revert EthBalanceInsufficient();
        _store.unspentOperationalFundAmount -= _amount;

        // Update prize pool's amount state var.

        season.totalPrizePoolAmount += _amount;

        // Emit events.

        emit OperationalFundsMovedToPrizePool(_nftType, _amount);
    }

    function withdrawOperationalFunds(StoreGetter.Store storage _store, uint256 _amount, address payable _recipient)
        external
    {
        // Validate input params.

        if (_amount == 0) revert StoreRevealCommit.ParamInvalid(0);
        if (_recipient == address(0)) revert StoreRevealCommit.ParamInvalid(1);

        // Validate signer's signature.
        // NOTE: We don't use signature here, because it can place unnecessary overhead on the operations, i.e. it adds the need of creating a streamline/UI interface/API for getting specific signatures for this function. Hence it's advisable to use an alternative, which is using a multisignature wallet, such as Gnosis.

        // bytes memory data = abi.encodePacked(_amount, _recipient);
        // Signature.verifySignature(_signer, data, _signature);

        // Validate and update operational fund's amount state var.

        if (_amount > _store.unspentOperationalFundAmount) revert EthBalanceInsufficient();
        _store.unspentOperationalFundAmount -= _amount;

        // NOTE: All the state changes should be done before this comment (e.g. to prevent reentrancy attack).

        // Send ETH amount to the recipient and validate it.

        _transferEth(_amount, _recipient);

        // Emit events.

        emit OperationalFundsWithdrawn(_amount, _recipient);
    }

    // Safe transfer of ETH to another account (wallet or smart contract), since it doesn't impose a gas limit.
    function _transferEth(uint256 _amount, address _sender)
        public
    {
        (bool sent, ) = _sender.call{value: _amount}("");
        if (!sent) revert StoreManager.EthWithdrawalFailed();
    }

}

// File: contracts/lib/StoreRevealCommit.sol



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

import "@openzeppelin/contracts/utils/Strings.sol";

import "./../lib/Conversion.sol";
import "./../lib/Signature.sol";
import "./../lib/StoreGetter.sol";

// NOTE: All these functions should be called in functions which are called only by (i.e. limited to) or signature gated by the operator.
library StoreRevealCommit {

    // =======================================================================
    // Libraries attached to var types.
    // =======================================================================

    using Strings for string;

    // =======================================================================
    // Errors.
    // =======================================================================

    error ParamInvalid(uint8 paramPosIdx);
    enum StateVars { SeasonEncryptedRngProvider, SeasonRevealedRngProvider }
    error StateVarAlreadySet(StateVars stateVar);

    // =======================================================================
    // Events.
    // =======================================================================

    event RngProviderCommited(StoreGetter.NftType nftType, bytes encryptedRngProvider);
    event RngProviderRevealed(StoreGetter.NftType nftType, address revealedRngProvider);

    // =======================================================================
    // Setter functions.
    // =======================================================================

    function commitRngProvider(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, bytes memory _encryptedRngProvider, string memory _encryptedRngProviderString)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(_store, _nftType);

        if (_encryptedRngProvider.length == 0) revert ParamInvalid(2);
        // NOTE: We use `_encryptedRngProviderString` param as the parsed (not converted) `string` equivalent of `bytes` param `_encryptedRngProvider`, becasuse verifying signed signature params (which we do later on) works only if they are provided in the `string` format, instead of `bytes` format. Because we can't include the `_encryptedRngProvider` param in the signature, we verify separately that it's equivalent to its parsed `string` type param/version `_encryptedRngProviderString`.
        string memory encryptedRngProviderString = Conversion.parseBytesAsString(abi.encodePacked(_encryptedRngProvider));
        if(!encryptedRngProviderString.equal(_encryptedRngProviderString)) revert ParamInvalid(3);

        // Validate signer's signature.
        // NOTE: We don't use signature here, because it can place unnecessary overhead on the operations, i.e. it adds the need of creating a streamline/UI interface/API for getting specific signatures for this function. Hence it's advisable to use an alternative, which is using a multisignature wallet, such as Gnosis.

        // We use and verify the calling function call signature to prevent a different sender from committing some encrypted data.
        // NOTE: Singer and sender aren't part of the signature, because the calling function deduces them, hence it doesn't get them via its function params.
        // bytes memory data = abi.encodePacked(_nftType.sport, _nftType.league, _nftType.season, _encryptedRngProviderString);
        // Signature.verifySignature(_signer, data, _signature);

        // Validate season's state vars.

        if (season.encryptedRngProvider.length > 0) revert StateVarAlreadySet(StateVars.SeasonEncryptedRngProvider);

        // Set season's state vars.

        // Commit the encrypted (more precisely, signed) version of the secret data.
        season.encryptedRngProvider = _encryptedRngProvider;

        // Emit events.

        emit RngProviderCommited(_nftType, _encryptedRngProvider);
    }

    function revealRngProvider(StoreGetter.Store storage _store, StoreGetter.NftType memory _nftType, address _revealedRngProvider, address _signer)
        external
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 3-rd/season node) is done later on. But `NftType` struct's excess params are allowed to be non-default, hence potentially breaking the event filtering logic when filtering by the `NftType` struct, if an event including a `NftType` struct param is emitted within this function.
        // StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(_store, _nftType);

        if (_revealedRngProvider == address(0)) revert ParamInvalid(2);

        // Validate signer's signatures.

        // NOTE: We don't use signature here, because it can place unnecessary overhead on the operations, i.e. it adds the need of creating a streamline/UI interface/API for getting specific signatures for this function. Hence it's advisable to use an alternative, which is using a multisignature wallet, such as Gnosis.
        // We use and verify the calling function call signature to prevent a different sender from revealing the encrypted data, in case it somehow comes into its possession.
        // NOTE: Singer and sender aren't part of the signature, because the calling function deduces them, hence it doesn't get them via its function params.
        // bytes memory data = abi.encodePacked(_nftType.sport, _nftType.league, _nftType.season, _revealedRngProvider);
        // Signature.verifySignature(_signer, data, _signature);

        // Validate season's state vars.

        if (season.revealedRngProvider != address(0)) revert StateVarAlreadySet(StateVars.SeasonRevealedRngProvider);

        // Verify the previously committed encrypted (more precisely, signed) version of the secret data matches the unencrypted/unsigned/revealed one.
        bytes memory data = abi.encodePacked(_revealedRngProvider);
        Signature.verifySignature(_signer, data, season.encryptedRngProvider);

        // Set season's state vars.

        season.revealedRngProvider = _revealedRngProvider;

        // Emit events.

        emit RngProviderRevealed(_nftType, _revealedRngProvider);
    }

}

// File: contracts/nft_store/AkshunStore.sol



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

import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

import "./AkshunStoreBuilder.sol";

import "./../lib/StoreManager.sol";

// NOTE: With multiple inheritance, there is an issue caused by the Diamond Problem, so the order of inheritance is important to avoid it. Solidity solves this problem like Python in that it uses C3 Linearization to force a specific order in the directed acyclic graph (DAG) of base classes. This results in the desirable property of monotonicity but disallows some inheritance graphs, especially, the order in which the base classes are given in the `is` directive is important. You have to list the direct base contracts in the order from "most base-like" (i.e. classes on the top level of inheritance) to "most derived" (i.e. classes on lower levels). Note that this order is the reverse of the one used in Python. When a function is called that is defined multiple times in different contracts, the given bases are searched from right to left (left to right in Python) in a depth-first manner, stopping at the first match. If a base contract has already been searched, it is skipped. E.g. in `contract C is A, B {}`, `C` requests `B` to override `A` (by specifying `A, B` in this order), and then `C` overrides `B`. Note that this means the default preference is to specify the inherited interfaces and directly inherited/dependent (abstract) contracts at the beginning. See https://docs.soliditylang.org/en/develop/contracts.html#multiple-inheritance-and-linearization
contract AkshunStore is AkshunStoreBuilder, ReentrancyGuard, Pausable {

    // =======================================================================
    // Libraries attached to var types.
    // =======================================================================

    using Strings for string;

    // =======================================================================
    // Errors.
    // =======================================================================

    error StateVarUnset(StoreRevealCommit.StateVars stateVar);
    error PublicSaleUnset();

    error EthDepositInsufficient();
    enum MintLimitTypes { Tier, Unlocked, Free, Wallet }
    error MintLimitReached(MintLimitTypes mintLimitType);

    // =======================================================================
    // Events.
    // =======================================================================
    // NOTE: Indexed event parameters that are not value types, i.e. arrays and structs, are not stored directly but instead a Keccak-256 hash of an encoding is stored. Specifically, the encoding of a struct is the concatenation of the (hashed) encoding of its members, always padded to a multiple of 32 bytes (even `bytes` and `string` types). Hence the indexed arguments for simple types will be in the event topics as is, for example `address`, `uint`, `bytes`; but types like `string` will be hashed with keccak256 and you will not be able to recover them. Hence we don't index `struct`, `[]`, `string` and `bytes` types (some of them, e.g. struct with only one filed, can be recovered though).

    event RngProviderCommited(StoreGetter.NftType nftType, bytes encryptedRngProvider, address indexed sender);
    event RngProviderRevealed(StoreGetter.NftType nftType, address revealedRngProvider, address indexed sender);

    event SeasonPassMinted(StoreGetter.NftType nftType, uint24 quantity, uint256 cost, uint8 tokenBaseURIIdx, uint256 indexed firstNewSeasonPassId, uint256 indexed lastNewSeasonPassId, address sender, address indexed recipient);
    event SeasonPassSwapped(StoreGetter.NftType nftType, uint256[] burnedSeasonPassIds, uint8 tokenBaseURIIdx, uint256 indexed firstNewSeasonPassId, uint256 indexed lastNewSeasonPassId, address sender, address indexed recipient);

    event PrizesSetOrUpdated(StoreGetter.NftType nftType, string[] prizeTypes, uint16[] prizeTypePcts);
    event PrizeClaimed(StoreGetter.NftType nftType, uint256 indexed seasonPassId, uint8 indexed prizeType, uint40 prizeTimestamp, uint256 amount, address sender, address indexed recipient);
    event OperationalFundsMovedToPrizePool(StoreGetter.NftType nftType, uint256 amount, address indexed signer, address indexed sender);

    // =======================================================================
    // General contract management functions.
    // =======================================================================

    // constructor()
    // {
    //     // Call initializers.
    //     // NOTE: By default, the owner of an Ownable contract is the account that deployed it.
    //
    //     // Create instances of interfaced contracts and libraries.
    //
    //     // Set general management state vars.
    //
    // }

    function pause()
        external
        onlyOwner
    {
        _pause();
    }

    function unpause()
        external
        onlyOwner
    {
        _unpause();
    }

    // =======================================================================
    // Setter functions.
    // =======================================================================

    // Secrets management.

    function commitRngProvider(StoreGetter.NftType memory _nftType, bytes memory _encryptedRngProvider, string memory _encryptedRngProviderString)
        external
        onlyOwner
    {
        // NOTE: Only operator can commit the encrypted version of the RNG provider (i.e. sign TX), hence the `_encryptedRngProvider` is assumed to be encrypted by his wallet.

        // Commit the encrypted (more precisely, signed) version of the secret data.
        StoreManager.commitRngProvider(store, _nftType, _encryptedRngProvider, _encryptedRngProviderString);
    }

    function revealRngProvider(StoreGetter.NftType memory _nftType, address _revealedRngProvider)
        external
        onlyOwner
    {
        // Set sender and signer.

        // NOTE: Sender already validated via `onlyOwner` modifier.
        // address sender = _msgSender();

        // NOTE: Only operator can reveal (and before that commit the encrypted version of) the RNG provider (i.e. sign TX).
        address signer = owner();

        // Set season's state vars.

        // Verify the previously committed encrypted (more precisely, signed) version of the secret data matches the unencrypted/unsigned/revealed one.
        StoreManager.revealRngProvider(store, _nftType, _revealedRngProvider, signer);
    }

    // Minting & burning management.

    function updateTierMaxSupplyLock(StoreGetter.NftType memory _nftType, uint24 _unlockedMaxSupply)
        external
        onlyOwner
    {
        StoreManager.updateTierMaxSupplyLock(store, _nftType, _unlockedMaxSupply);
    }

    // NOTE: Should be enabled only temporarily for a particular free mint batch to be minted, for security reasons.
    function updateCanAutomatorSignFreeMints(bool _canAutomatorSignFreeMints)
        external
        onlyOwner
    {
        StoreManager.updateCanAutomatorSignFreeMints(store, _canAutomatorSignFreeMints);
    }

    function mintSeasonPasses(StoreGetter.NftType memory _nftType, uint24 _quantity, bool _free, uint8 _tokenBaseURIIdx, address _recipient, uint40 _mintTimestamp, bytes memory _signature)
        external
        payable
        whenNotPaused
        nonReentrant
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 5-th/tier node) is done later on.
        // StoreGetter._validateNftType(_nftType, 5);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);
        StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);
        StoreGetter.Tier storage tier = StoreGetter._getTier(store, _nftType);

        // NOTE: `_quantity` validation is done later instead, when the quantity is adjusted based on the available quantity.
        // if (_quantity == 0) revert ParamInvalid(1);
        // NOTE: `_free` and `_tokenBaseURIIdx` can't be validated, since their default values are also valid.
        // NOTE: Redundant validation, since `season.nftContract.mint` method already does it.
        // if (_recipient == address(0)) revert ParamInvalid(4);
        if (_mintTimestamp == 0) revert ParamInvalid(5);

        // Validate sender and signer's signature.

        { // NOTE: Scope to avoid stack too deep error.
        // Declare this var inside of this scope to avoid stack too deep error.
        address sender = _msgSender();

        // NOTE: Only operator can mint for free (i.e. sign TX), unless permitted otherwise.
        address signer = _free && !store.canAutomatorSignFreeMints || sender == owner() ? owner() : defaultSigner;
        bytes memory data = bytes.concat(StoreGetter._packNftTypeFields(_nftType), abi.encodePacked(_quantity, _free, _tokenBaseURIIdx, _recipient, _mintTimestamp));
        uint40 signatureExpiryTimestamp = _mintTimestamp + season.mintPeriodMins * 60;
        _verifyNonOwnerCallSignature(signer, sender, data, _signature, signatureExpiryTimestamp);

        } { // NOTE: Scope to avoid stack too deep error.
        // Validate the RNG provider was commited before the minting starts.

        if (season.encryptedRngProvider.length == 0) revert StateVarUnset(StoreRevealCommit.StateVars.SeasonEncryptedRngProvider);

        // Validate and compute mint quantity.

        // NOTE: We don't need to also check for `season.maxSupply`, since `tier.maxSupply`, which is a stricter condition, is enough.
        // NOTE: Checking `tierUnsold` is not needed, since, `tierUnlockedUnsold` (or `seasonFreeUnsold` in case of a free mint) is a stricter condition (always a lower or the same value), but we use it so we can revert with a more precise error `MintLimitReached`.
        uint24 tierUnsold = tier.maxSupply - tier.sold;
        if (tierUnsold == 0) revert MintLimitReached(MintLimitTypes.Tier);
        // NOTE: Not needed; check the explanation above.
        // if (_quantity > tierUnsold) {
        //     _quantity = tierUnsold;
        // }

        // NOTE: We can free mint even if the requested tier quantity is locked (but it gets updated). This is needed, because otherwise if we would unlock an additional (tier's) NFT quantity in order to free mint it, other users could replay the old signed but not yet sent/broadcasted mint TX messages, effectively minting some or all of the tier's quantities we reserved for free mints.
        uint24 seasonFreeUnsold;
        uint24 tierUnlockedUnsold = tier.unlockedMaxSupply - tier.sold;
        if (!_free) {
            if (tierUnlockedUnsold == 0) revert MintLimitReached(MintLimitTypes.Unlocked);
            if (_quantity > tierUnlockedUnsold) {
                _quantity = tierUnlockedUnsold;
            }
        } else {
            seasonFreeUnsold = season.maxFreeSupply - season.freeSold;
            if (seasonFreeUnsold == 0) revert MintLimitReached(MintLimitTypes.Free);
            if (_quantity > seasonFreeUnsold) {
                _quantity = seasonFreeUnsold;
            }
        }

        uint24 salePerWalletUnsold = sale.maxPerWalletSupply - sale.perWalletSold[_recipient];
        if (salePerWalletUnsold == 0) revert MintLimitReached(MintLimitTypes.Wallet);
        if (_quantity > salePerWalletUnsold) {
            _quantity = salePerWalletUnsold;
        }

        if (_quantity == 0) revert ParamInvalid(1);

        // Update quantity's state vars.
        // NOTE: Helper vars need to always be updated as well, besides the dependent var.

        tier.sold += _quantity;
        sale.hSold += _quantity;
        season.hSold += _quantity;

        if (_free) {
            if (_quantity > tierUnlockedUnsold) {
                tier.unlockedMaxSupply = tier.sold;
            }

            season.freeSold += _quantity;
            // NOTE: Can't subtract unsigned ints before validating the result is non-negative, since an underflow error would be thrown.
            uint24 seasonUnsold = season.maxSupply - season.hSold;
            if (seasonFreeUnsold > seasonUnsold) {
                season.maxFreeSupply -= seasonFreeUnsold - seasonUnsold;
            }
        }

        uint24 salePerWalletSold = sale.perWalletSold[_recipient] += _quantity;
        if (salePerWalletSold > sale.hMaxPerWalletSold) {
            sale.hMaxPerWalletSold = salePerWalletSold;
        }

        } // NOTE: Scope to avoid stack too deep error.
        // Validate and read mint cost.

        uint256 cost = _quantity * (_free ? 0 : tier.price);
        if (msg.value < cost) revert EthDepositInsufficient();

        { // NOTE: Scope to avoid stack too deep error.
        // Update cost's state vars.

        // NOTE: Division must always be the last operation, because decimals are truncated, since we are computing with integers.
        uint256 addedPrizePoolAmount = cost * season.prizePoolBps / 1e4;
        season.totalPrizePoolAmount += addedPrizePoolAmount;
        store.unspentOperationalFundAmount += cost - addedPrizePoolAmount;

        } // NOTE: Scope to avoid stack too deep error.
        // Update general management state vars.

        senderNonces[_msgSender()]++;

        // NOTE: All the state changes should be done before this comment (e.g. to prevent reentrancy attack).

        _hMintAndReturnOverpayment(_nftType, season, sale, _quantity, cost, tier.tokenPresetURIIdx, _tokenBaseURIIdx, _msgSender(), _recipient);
    }

    function _hMintAndReturnOverpayment(StoreGetter.NftType memory _nftType, StoreGetter.Season storage _season, StoreGetter.Sale storage _sale, uint24 _quantity, uint256 _cost, uint8 _tokenPresetURIIdx, uint8 _tokenBaseURIIdx, address _sender, address _recipient)
        private
    {
        // Send overpaid ETH amount back to the sender and validate it, if applicable.

        uint256 overpaidCost = msg.value - _cost;
        if(overpaidCost > 0) {
            StoreManager._transferEth(overpaidCost, _sender);
        }

        // Mint new Season Pass NFT to the recipient.

        (uint256 lastNewSeasonPassId, uint256 firstNewSeasonPassId) = _mintSeasonPasses(_nftType, _season, _sale, _quantity, _tokenPresetURIIdx, _tokenBaseURIIdx, _recipient);

        // Emit events.

        emit SeasonPassMinted(_nftType, _quantity, _cost, _tokenBaseURIIdx, firstNewSeasonPassId, lastNewSeasonPassId, _sender, _recipient);
    }

    function _mintSeasonPasses(StoreGetter.NftType memory _nftType, StoreGetter.Season storage _season, StoreGetter.Sale storage _sale, uint24 _quantity, uint8 _tokenPresetURIIdx, uint8 _tokenBaseURIIdx, address _recipient)
        private
        returns(uint256 _lastNewSeasonPassId, uint256 _firstNewSeasonPassId)
    {
        for (uint24 i = 0; i < _quantity;) {
            _lastNewSeasonPassId = _season.nftContract.mint(_season.subcollectionIdx, _tokenPresetURIIdx, _tokenBaseURIIdx, _recipient);

            _sale.tokenTiers[_lastNewSeasonPassId] = _nftType.tier;
            unchecked { i++; }
        }
        _firstNewSeasonPassId = _lastNewSeasonPassId - (_quantity - 1);
    }

    function swapSeasonPasses(StoreGetter.NftType memory _nftType, uint256[] memory _seasonPassIds, uint24 _quantity, uint8 _tokenBaseURIIdx, address _recipient, uint40 _swapTimestamp, bytes memory _signature)
        external
        whenNotPaused
        nonReentrant
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 5-th/tier node) is done later on.
        // StoreGetter._validateNftType(_nftType, 5);

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);
        StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);
        // NOTE: We don't need to check existence of the tier via `_nftType.tier`, because we check it later via `sale.tokenTiers`. But we use the tier instance later on.

        if (_seasonPassIds.length == 0) revert ParamInvalid(1);
        // We can't mint more Season Pass NFTs than we burn, since that would enlarge the season collection beyond it's quantity limits. And we can only mint new Season Pass NFTs which are of the same NFT type, to assure no quantity limits are broken.
        if (_quantity > _seasonPassIds.length) revert ParamInvalid(2);
        // NOTE: The swapped Season Passes will never have the first, i.e. `0`, token's base URI, since it's always a newer than the initial base URI (for the corresponding season).
        if (_tokenBaseURIIdx == 0) revert ParamInvalid(3);
        // NOTE: Redundant validation, since `season.nftContract.mint` method already does it.
        // if (_recipient == address(0)) revert ParamInvalid(4);
        if (_swapTimestamp == 0) revert ParamInvalid(5);

        // Validate sender and signer's signature.

        { // NOTE: Scope to avoid stack too deep error.
        // Declare this var inside of this scope to avoid stack too deep error.
        address sender = _msgSender();

        address signer = sender == owner() ? owner() : defaultSigner;
        // NOTE: `_nftType.sale` and `_nftType.tier` aren't required in the signature, as explained before.
        bytes memory data = bytes.concat(StoreGetter._packNftTypeFields(_nftType), abi.encodePacked(_seasonPassIds, _quantity, _tokenBaseURIIdx, _recipient, _swapTimestamp));
        uint40 signatureExpiryTimestamp = _swapTimestamp + season.mintPeriodMins * 60;
        _verifyNonOwnerCallSignature(signer, sender, data, _signature, signatureExpiryTimestamp);

        } // NOTE: Scope to avoid stack too deep error.
        // Update general management state vars.

        senderNonces[_msgSender()]++;

        // NOTE: All the state changes should be done before this comment (e.g. to prevent reentrancy attack).

        // Burn the old sender's Season Pass NFTs.

        _burnSeasonPasses(_nftType, season, sale, _seasonPassIds);

        // Mint new Season Pass NFT to the recipient.

        StoreGetter.Tier storage tier = StoreGetter._getTier(store, _nftType);
        (uint256 lastNewSeasonPassId, uint256 firstNewSeasonPassId) = _mintSeasonPasses(_nftType, season, sale, _quantity, tier.tokenPresetURIIdx, _tokenBaseURIIdx, _recipient);

        // Emit events.

        emit SeasonPassSwapped(_nftType, _seasonPassIds, _tokenBaseURIIdx, firstNewSeasonPassId, lastNewSeasonPassId, _msgSender(), _recipient);
    }

    function _burnSeasonPasses(StoreGetter.NftType memory _nftType, StoreGetter.Season storage _season, StoreGetter.Sale storage _sale, uint256[] memory _seasonPassIds)
        private
    {
        // NOTE: `_seasonPassIds` is a dynamic/unlimited array, but we implicitly limit them to `uint24` possible values and for the iterator we use the same `uint24` type as the dependent `Season.maxSupply` field.
        for (uint24 i = 0; i < _seasonPassIds.length;) {
            uint8 tier = _sale.tokenTiers[_seasonPassIds[i]];
            // NOTE: We could just check if all the to-be burnt Season Pass NFTs exist in the sale and are of the same NFT type, but by also checking against `_nftType.tier` we are more explicit and hence more bullet proof, plus `_nftType` can't have invalid values this way, which would break the filtering logic of the event emmiting it.
            if (tier == 0 || tier != _nftType.tier) revert ArrayParamInvalid(1, i);
            // NOTE: Redundant validation, since `_season.nftContract.burn` method already does it (via `ERC721.ownerOf`).
            // if (_seasonPassIds[i] == 0) revert ArrayParamInvalid(1, i);
            // NOTE: Redundant validation, since `_season.nftContract.burn` method already does it (via `ERC721._isApprovedOrOwner`), and also allows the approved addresses to burn the token.
            // _season.nftContract.ownerOf(_seasonPassIds[i]);

            _season.nftContract.burn(_seasonPassIds[i]);
            unchecked {  i++; }
        }
    }

    // Prizes management.

    function addPrizeTypes(StoreGetter.NftType memory _nftType, string[] memory _prizeTypes)
        external
        onlyOwner
    {
        StoreManager.addPrizeTypes(store, _nftType, _prizeTypes);
    }

    function setOrUpdatePrizes(StoreGetter.NftType memory _nftType, uint24 _prizeClaimPeriodMins, uint16[] memory _prizeTypePcts)
        external
        onlyOwner
    {
        StoreManager.setOrUpdatePrizes(store, _nftType, _prizeClaimPeriodMins, _prizeTypePcts);
    }

    function updatePrizePoolLock(StoreGetter.NftType memory _nftType, uint16 _unlockedPrizePoolBps)
        external
        onlyOwner
    {
        StoreManager.updatePrizePoolLock(store, _nftType, _unlockedPrizePoolBps);
    }

    function claimPrize(StoreGetter.NftType memory _nftType, uint256 _seasonPassId, uint8 _prizeType, uint40 _prizeTimestamp, uint256 _amount, bool _canUseOperationalFundAmount, address payable _recipient, bytes memory _signature)
        external
        whenNotPaused
        nonReentrant
    {
        // Validate input params.

        // NOTE: Not needed, since checking for the existance of nodes (specifically, including the last, 5-th/tier node) is done later on.
        // StoreGetter._validateNftType(_nftType, 5);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);
        // NOTE: We don't need to check which sale is set via `_nftType.sale` (i.e. to which sale the `_seasonPassId` Season Pass NFT belongs to, given that the right one was provided to this function call), because the prizes are always based on the Public sale/stage. But we need it later on to get the Season Pass NFT's tier.
        // NOTE: We intentionally don't call `StoreGetter._getSale(store, _nftType)` instead, to return a more informative `PublicSaleUnset()` error, instead of `NodeNonexistent(4)` error.
        StoreGetter.Sale storage publicSale = season.sales[StoreGetter.publicSaleName];
        if (!publicSale.hExists) revert PublicSaleUnset();
        // NOTE: We don't need to check existence of the tier via `_nftType.tier`, because we check it later via `sale.tokenTiers`.

        // NOTE: The prize is bound to the particular Season Pass NFT.
        if (_seasonPassId == 0) revert ParamInvalid(1);
        // NOTE: `_amount` param should be `> 0` only when `_prizeType` is `0`, since the first `Sport.prizeTypes` is always a custom prize with a custom (not predefined) amount.
        if ((_prizeType == 0) == (_amount == 0)) revert ParamInvalid(2);
        if (_prizeTimestamp == 0) revert ParamInvalid(3);
        // NOTE: `_amount` was already validated with `_prizeType`, and `_canUseOperationalFundAmount` can't be validated, since its default value is also valid.
        if (_recipient == address(0)) revert ParamInvalid(6);

        // Validate sender and signer's signature.

        address sender = _msgSender();

        { // NOTE: Scope to avoid stack too deep error.
        // NOTE: Only operator can spend operational funds (i.e. sign TX).
        address signer = _canUseOperationalFundAmount || sender == owner() ? owner() : defaultSigner;
        // NOTE: `_nftType.sale` and `_nftType.tier` aren't required in the signature, as explained before.
        bytes memory data = bytes.concat(StoreGetter._packNftTypeFields(_nftType), abi.encodePacked(_seasonPassId, _prizeType, _prizeTimestamp, _amount, _canUseOperationalFundAmount, _recipient));
        uint40 signatureExpiryTimestamp = _prizeTimestamp + season.prizeClaimPeriodMins * 60;
        _verifyNonOwnerCallSignature(signer, sender, data, _signature, signatureExpiryTimestamp);

        // Validate ownership of the winning Season Pass NFT.

        // NOTE: The Season Pass NFT existance check (i.e. `seasonPassOwner == address(0)`) could revert separately with e.g. `TokenNonexistent()` error, but we combine the checks to reduce gas costs.
        address seasonPassOwner = season.nftContract.ownerOf(_seasonPassId);
        if (seasonPassOwner == address(0) || sender != seasonPassOwner && (_recipient != seasonPassOwner || sender != defaultSigner && sender != owner())) revert SenderInvalid();

        } // NOTE: Scope to avoid stack too deep error.
        // Validate and compute prize amount.

        if (_prizeType >= season.prizeTypePcts.length) revert ParamInvalid(2);

        uint256 amount;
        if (_prizeType == 0) {
            // The first `Sport.prizeTypes` is always the custom prize type (i.e. `StoreGetter.customPrizeType`) with a custom (not predefined) amount.
            amount = _amount;
        } else {
            StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);
            uint8 tier = sale.tokenTiers[_seasonPassId];
            // NOTE: We could just check if the to-be burnt Season Pass NFT exists in the sale, but by also checking against `_nftType.tier` we are more explicit and hence more bullet proof, plus `_nftType` can't have invalid values this way, which would break the filtering logic of the event emmiting it.
            if (tier == 0 || tier != _nftType.tier) revert ParamInvalid(1);

            // NOTE/TODO: We need to make sure the Public sale/stage has as many tiers as the sale with the most tiers, otherwise an exception can be thrown for accessing the array at an out-of-bounds index.
            amount = publicSale.tiers[tier - 1].price * season.prizeTypePcts[_prizeType] / 100;
        }

        { // NOTE: Scope to avoid stack too deep error.
        // Validate and update prize pool's and operational fund's amount state vars.

        // NOTE: Division must always be the last operation, because decimals are truncated, since we are computing with integers.
        uint256 unlockedPrizePoolAmount = season.totalPrizePoolAmount * season.unlockedPrizePoolBps / 1e4;
        uint256 unspentPrizePoolAmount = unlockedPrizePoolAmount - season.spentPrizePoolAmount;
        if (amount <= unspentPrizePoolAmount) {
            season.spentPrizePoolAmount += amount;
        } else {
            // Use the rest of the prize pool funds when claiming prize from the operational fund.
            uint256 extraSpentOperationalFundAmount = amount - unspentPrizePoolAmount;
            if (!_canUseOperationalFundAmount || extraSpentOperationalFundAmount > store.unspentOperationalFundAmount) revert StoreManager.EthBalanceInsufficient();

            season.spentPrizePoolAmount = unlockedPrizePoolAmount;
            store.unspentOperationalFundAmount -= extraSpentOperationalFundAmount;
        }

        // Update prize state vars.

        // NOTE: Check if it's cheaper to always set it, without checking it's value. Storing values is expensive in Solidity, so probably it's not.
        if (!season.prizeTypesUsed[_prizeType]) {
            season.prizeTypesUsed[_prizeType] = true;
        }

        // Update general management state vars.

        senderNonces[sender]++;

        // NOTE: All the state changes should be done before this comment (e.g. to prevent reentrancy attack).

        // Send ETH amount to the recipient and validate it.

        StoreManager._transferEth(amount, _recipient);

        } // NOTE: Scope to avoid stack too deep error.
        // Emit events.

        emit PrizeClaimed(_nftType, _seasonPassId, _prizeType, _prizeTimestamp, amount, sender, _recipient);
    }

    // Operational funds management.

    function moveOperationalFundsToPrizePool(StoreGetter.NftType memory _nftType, uint256 _amount)
        external
        onlyOwner
    {
        StoreManager.moveOperationalFundsToPrizePool(store, _nftType, _amount);
    }

    function withdrawOperationalFunds(uint256 _amount, address payable _recipient)
        external
        onlyOwner
    {
        StoreManager.withdrawOperationalFunds(store, _amount, _recipient);
    }

    // NOTE: To enable depositing ETH directy to the contract's address, the contract must define at least one of the functions below. `receive()` is called if `msg.data` is empty, otherwise `fallback()` is called.

    // Function invoked when ETH is deposited directy to the contract's address and `msg.data` is empty.
    //
    // We use it to assign the ETH which was deposited directy to the contract's address, to the operational fund.
    //
    // NOTE: We assign the deposited ETH to the operational fund because then we can still transfer any of the available ETH amount from the operational fund to the prize pool.
    receive()
        external
        payable
    {
        // NOTE: There are no side effects if the sent amount is `0`; adding validation to prevent/revert such case would be less gas efficient.
        // NOTE: It could be safer (i.e. more stricter) to send the `msg` var itself and get the sent amount in the called function via `msg.value`, but the values of all members of `msg`, including `msg.sender` and `msg.value`, can change for every external function call. This includes calls to library functions.
        StoreManager.receiveOperationalFunds(store, msg.value);
    }

    // A fallback function, optionally payable (i.e. is defined with `payable` modifier), invoked when no other function matches the call (not even the `receive()` function, but only if `fallback()` function is payable), otherwise the contract will throw an exception. If it is payable, then it's also invoked when ETH is deposited directy to the contract's address and `msg.data` is either not empty or `msg.data` is empty and `receive()` method doesn't exists (i.e. isn't defined in the contract; in which case the `fallback()` function is restricted to 2300 gas, even if `call()` function was used to send ETH), otherwise the contract will throw an exception in both cases.
    //
    // NOTE: You should only need to implement the fallback function if you are following an upgrade or proxy pattern.
    // fallback()
    //     external
    //     payable
    // {}

    // =======================================================================
    // Getter functions.
    // =======================================================================

    function getPerSaleWalletSold(StoreGetter.NftType memory _nftType, address _user)
        external
        view
        returns(uint24)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);

        // Get state vars.

        return sale.perWalletSold[_user];
    }

}

// File: contracts/nft_store/AkshunStoreBuilder.sol



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

import "./../common/Signable.sol";

import "./../lib/StoreDeleter.sol";

// NOTE: For functions which are setting or updating the Store nodes we check for "existance", i.e. whether the Store node was already set and is trying to be updated now, instead of checking for "usage", e.g. whether the Store node was already used (e.g. when any of it's associated NFTs were already minted), because this way we prevent changing Store structure/settings in a way which would be worse for the user who might use the contract after this change, while assuming it wasn't changed (especially in the edge case where he would be the first one to execute the (singed) mint TX which was singed before this change). But we use the "usage" approach for functions which are deleting the Store nodes, since in that case e.g. the minting TX of the affected NFT type would fail/revert anyway.
// NOTE: Could also move all the code inside of set or update functions into a library called `StoreSetter`, and reduce this contract's size even further, but the gas costs would rise because of the calls to the libraries. See: https://ethereum.stackexchange.com/questions/101961/is-it-safe-to-mark-library-functions-that-modify-the-calling-contracts-state-as
abstract contract AkshunStoreBuilder is Signable {

    // =======================================================================
    // Libraries attached to var types.
    // =======================================================================

    using Strings for string;

    // =======================================================================
    // Errors.
    // =======================================================================

    error ArrayParamInvalid(uint8 paramPosIdx, uint256 arrayPosIdx);

    // =======================================================================
    // Events.
    // =======================================================================

    event SeasonSetOrUpdated(StoreGetter.NftType nftType, uint24 maxSupply, uint24 maxFreeSupply, uint24 mintPeriodMins, uint16 prizePoolBps);
    event SaleSetOrUpdated(StoreGetter.NftType nftType, bool tryout, uint24 maxPerWalletSupply, uint24[] tiersMaxSupply, uint256[] tiersPrice);
    event NodeDeleted(StoreGetter.NftType nftType, uint8 nodeDepth);

    // =======================================================================
    // State vars.
    // =======================================================================

    // Hierarchical store structure.
    // NOTE: Can't use `public` visibility, because this struct has all its members omitted, therefore the getter cannot return any values.
    StoreGetter.Store store;

    // =======================================================================
    // Setter functions.
    // =======================================================================

    function setOrUpdateSeason(address _nftContract, StoreGetter.NftType memory _nftType, uint24 _maxSupply, uint24 _maxFreeSupply, uint24 _mintPeriodMins, uint16 _prizePoolBps)
        external
        onlyOwner
    {
        // Validate input params.

        StoreGetter._validateNftType(_nftType, 3);

        // Validate existence of objects associated with the input params.
        // NOTE: We can't use `StoreNodes`'s getters, because our encompassing function also sets, not just updates the store's sport, league and season nodes, which means the TX would revert on (initial) setup. Hence we validate the NFT type fileds via `StoreGetter._validateNftType` validator.
        StoreGetter.Sport storage sport = store.sports[_nftType.sport];
        StoreGetter.League storage league = sport.leagues[_nftType.league];
        StoreGetter.Season storage season = league.seasons[_nftType.season];

        // NOTE: We could instead use `bool seasonUsed = season.hSold > 0`, but this way we prevent changing Store (particularly, season) structure/settings in a way which would be worse for the user who might use the contract after this change, while assuming it wasn't changed (especially in the edge case where he would be the first one to execute the (singed) mint TX which was singed before this change). But we use this "used" approach when deleting the Store nodes, since in that case the minting TX of the affected NFT type would fail/revert anyway. We use "exists" instead of "used" also for other set or update method (e.g. `setOrUpdateSale`).
        bool seasonExists = season.maxSupply > 0;
        if (_nftContract == address(0) || seasonExists && _nftContract != address(season.nftContract)) revert ParamInvalid(0);
        if (_maxSupply == 0 || seasonExists && (_maxSupply > season.maxSupply || _maxSupply < season.hSold || _maxSupply < season.hTotalSalesMaxSupply)) revert ParamInvalid(2);
        if (_maxFreeSupply > _maxSupply || seasonExists && (_maxFreeSupply > season.maxFreeSupply || _maxFreeSupply < season.freeSold)) revert ParamInvalid(3);
        if (_mintPeriodMins == 0 || _mintPeriodMins < season.mintPeriodMins) revert ParamInvalid(4);
        // NOTE: The current logic doesn't work correctly when prize pool BPS is updated. Hence, we only allow to set it.
        if (_prizePoolBps == 0 || _prizePoolBps > 10_000 || seasonExists && _prizePoolBps != season.prizePoolBps) revert ParamInvalid(5);

        // Update/set season's state vars.

        season.maxSupply = _maxSupply;
        season.maxFreeSupply = _maxFreeSupply;
        season.mintPeriodMins = _mintPeriodMins;

        if (!seasonExists) {
            season.subcollectionIdx = store.subcollectionsCnt++;
            season.nftContract = IAkshunSeasonPassNft(_nftContract);
            season.prizePoolBps = _prizePoolBps;

            // Update season's parent nodes' state vars.

            league.hSeasonNames.push(_nftType.season);

            if (!league.hExists) {
                league.hExists = true;
                sport.hLeagueNames.push(_nftType.league);

                if (!sport.hExists) {
                    sport.hExists = true;
                    store.hSportNames.push(_nftType.sport);
                }
            }
        }

        // Emit events.

        emit SeasonSetOrUpdated(_nftType, _maxSupply, _maxFreeSupply, _mintPeriodMins, _prizePoolBps);
    }

    function setOrUpdateSale(StoreGetter.NftType memory _nftType, uint8 _hSaleNameIdx, bool _tryout, uint24 _maxPerWalletSupply, uint8[] memory _tokenPresetURIIdxs, uint24[] memory _tiersMaxSupply, uint256[] memory _tiersPrice)
        external
        onlyOwner
    {
        // Validate input params.

        StoreGetter._validateNftType(_nftType, 4);

        // Validate existence of objects associated with the input params.
        // NOTE: We could just get the child store nodes from the parent ones directly without the getters, but they also perform the validation and reduce gas costs.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);
        // NOTE: We can't use `StoreNodes`'s getters, because our encompassing function also sets, not just updates the store's sale and tier nodes, which means the TX would revert on (initial) setup. Hence we validate the NFT type fileds via `StoreGetter._validateNftType` validator.
        StoreGetter.Sale storage sale = season.sales[_nftType.sale];

        if (_tokenPresetURIIdxs.length == 0) revert ParamInvalid(4);
        if (_tiersMaxSupply.length != _tokenPresetURIIdxs.length) revert ParamInvalid(5);
        if (_tiersPrice.length != _tiersMaxSupply.length) revert ParamInvalid(6);

        { // NOTE: Scope to avoid stack too deep error.
        // Validate sale's state vars.

        // NOTE: We intentionally don't call `StoreGetter._getSale(store, _nftType)` instead, to not raise `NodeNonexistent(4)` error.
        StoreGetter.Sale storage publicSale = season.sales[StoreGetter.publicSaleName];
        // Tryout sale phases can only be added after the Public sale phase, and they can't be switched to non-tryout and vice versa.
        if (sale.hExists && sale.tryout != _tryout || _tryout && !publicSale.hExists) revert ParamInvalid(2);

        } // NOTE: Scope to avoid stack too deep error.
        // Previous sale's tiers' quantities and prices must be better for the user (i.e. scarcer quantity and cheaper price), to incentivize minting at an early stage. Except for the tryout Season Pass NFTs, of which prices and quantities are not validated against the previous sale (because the prices, and probably also quantities, will be much smaller; and the prizes will be lesser).
        // NOTE: We use `_hSaleNameIdx` helper param to avoid iterating over `season.sales` array until finding the (potential) previous sale. Could be avoided with a different approach.
        if (!sale.hExists && _hSaleNameIdx != season.hSaleNames.length || sale.hExists && (_hSaleNameIdx >= season.hSaleNames.length || !season.hSaleNames[_hSaleNameIdx].equal(_nftType.sale))) revert ParamInvalid(1);
        if (_hSaleNameIdx > 0 && !_tryout) {
            StoreGetter.Sale storage previousSale = season.sales[season.hSaleNames[_hSaleNameIdx - 1]];
            StoreGetter.Tier storage previousSaleFirstTier = previousSale.tiers[0];
            StoreGetter.Tier storage previousSaleLastTier = previousSale.tiers[previousSale.tiers.length - 1];

            if (_tiersMaxSupply[0] < previousSaleFirstTier.maxSupply || _tiersMaxSupply[_tiersMaxSupply.length - 1] < previousSaleLastTier.maxSupply) revert ParamInvalid(5);
            if (_tiersPrice[0] < previousSaleFirstTier.price || _tiersPrice[_tiersPrice.length - 1] < previousSaleLastTier.price) revert ParamInvalid(6);
        }

        // Validate and delete/update/set sale's state vars.

        if (_tiersMaxSupply.length < sale.tiers.length) {
            // NOTE: We implicitly limit the `sale.tiers` array (and hence the dependent `_tiersMaxSupply` array) size to `uint8` possible values via `NftType.tier` field.
            for (uint8 i = uint8(sale.tiers.length) - 1; i >= _tiersMaxSupply.length;) {
                // Validate excess old tier's state vars.
                StoreGetter.Tier storage excessOldTier = sale.tiers[i];
                if (excessOldTier.sold > 0) revert StoreDeleter.LeafNodeAlreadyUsed(i);

                // Read excess old tier's state vars before they are deleted/reset. Update sale's and sale's parent nodes' state vars.
                // NOTE: Helper vars need to always be updated as well, besides the dependent var.
                sale.hMaxSupply -= excessOldTier.maxSupply;
                season.hTotalSalesMaxSupply -= excessOldTier.maxSupply;

                // Delete excess old tier's state vars.
                sale.tiers.pop();
                unchecked { i--; }
            }
        }

        uint24 hSaleMaxSupply = 0;
        bool previousTierExists;
        uint24 previousTierMaxSupply;
        uint256 previousTierPrice;
        // NOTE: We implicitly limit the `sale.tiers` array (and hence the dependent `_tiersMaxSupply` array) size to `uint8` possible values via `NftType.tier` field.
        for (uint8 i = 0; i < _tiersMaxSupply.length;) {
            // Validate old/new tier's state vars.
            StoreGetter.Tier memory oldTier; // NOTE: Can't use `storage`, because a storage var always needs to point to an existing state variable.
            bool oldTierSoldAny;
            bool tierExists = i < sale.tiers.length;
            if (tierExists) {
                oldTier = sale.tiers[i];
                oldTierSoldAny = oldTier.sold > 0;
            }

            // NOTE: The rules for updating preset URIs are relaxed, since they aren't so important, because the actual/revealed base URIs are later assigned permanently (to each token).
            if (i > 0 && _tokenPresetURIIdxs[i] <= _tokenPresetURIIdxs[i - 1]) revert ArrayParamInvalid(4, i);
            // Previous sale's tier quantity must be better for the user (i.e. scarcer quantity), but the price must be worse for the user (i.e. more expensive price), since the payout is bigger, to incentivize minting of sale's higher tier grades (i.e. tiers with lower tier number). If a sale's lower tier grade has already some Season Pass NFTs sold and gets updated, its quantity must get better for the user (i.e. scarcer quantity), to incentivize minting of this sale's tier grade, but its price must remain the same, to not harm the users who already bought this tier's Season Pass NFTs. Note that if the tier hasn't sold any Season Pass NFTs yet, we can update both its quantity and price with the same validation rules as if it would be added (for the first time) instead, because in such case any change to the validation logic doesn't make sense, since the tier can also be deleted via `StoreDeleter.deleteSale` method and then readded via this method.
            // NOTE: Even though you can later reduce the quantity of the tiers' supply, it can't be lower than the previous tier, and can't be lower than the previous sale's last tier, and the lowest quantity the first sale's first tier can have is 1, and the lowest quantity the last sale's last tier can have equals the previous tier's quantity (if it's the only tier in a sale, than the previous tier is the previous sale's last tier). A use case for lowering the quantities is when the different sales and tiers wouldn't get sold out.
            if (_tiersMaxSupply[i] == 0 || previousTierExists && _tiersMaxSupply[i] < previousTierMaxSupply || oldTierSoldAny && (_tiersMaxSupply[i] < oldTier.sold || _tiersMaxSupply[i] > oldTier.maxSupply)) revert ArrayParamInvalid(5, i);
            if (_tiersPrice[i] == 0 || previousTierExists && _tiersPrice[i] > previousTierPrice || oldTierSoldAny && _tiersPrice[i] != oldTier.price) revert ArrayParamInvalid(6, i);

            hSaleMaxSupply += _tiersMaxSupply[i];
            if (tierExists) {
                // Read old tier's state vars before they are updated.
                // NOTE: Not needed.

                // Update old tier's state vars.
                oldTier.maxSupply = _tiersMaxSupply[i];
                if (oldTier.unlockedMaxSupply > oldTier.maxSupply) {
                    oldTier.unlockedMaxSupply = oldTier.maxSupply;
                }
                oldTier.price = _tiersPrice[i];
                sale.tiers[i] = oldTier;
            } else {
                // Set new tier's state vars.
                StoreGetter.Tier memory newTier = StoreGetter.Tier(_tokenPresetURIIdxs[i], _tiersMaxSupply[i], 0, 0, _tiersPrice[i]);
                sale.tiers.push(newTier);
            }

            previousTierExists = tierExists;
            previousTierMaxSupply = _tiersMaxSupply[i];
            previousTierPrice = _tiersPrice[i];
            unchecked { i++; }
        }

        // Update/set and validate sale's and sale's parent nodes' state vars.

        if (_maxPerWalletSupply == 0 || _maxPerWalletSupply > hSaleMaxSupply || sale.hExists && (_maxPerWalletSupply > sale.maxPerWalletSupply || _maxPerWalletSupply < sale.hMaxPerWalletSold)) revert ParamInvalid(3);
        // NOTE: When subtracting `uint<number>`s, do it in a way the result and any intermediary calculations can't be negative, to prevent arithmetic underflow error. Hence we need to first add to the var the new value(s) and then, always at the end, subtract from it the old one(s).
        // Wrong:
        // `season.hTotalSalesMaxSupply += hSaleMaxSupply - sale.hMaxSupply;`
        // Correct:
        // `season.hTotalSalesMaxSupply += hSaleMaxSupply;`
        // `season.hTotalSalesMaxSupply -= sale.hMaxSupply;`
        // Or:
        season.hTotalSalesMaxSupply = season.hTotalSalesMaxSupply + hSaleMaxSupply - sale.hMaxSupply;
        if (season.hTotalSalesMaxSupply > season.maxSupply) revert ParamInvalid(5);

        if (!sale.hExists) {
            season.hSaleNames.push(_nftType.sale);

            sale.hExists = true;
            sale.tryout = _tryout;
        }
        sale.hMaxSupply = hSaleMaxSupply;
        sale.maxPerWalletSupply = _maxPerWalletSupply;

        // Emit events.

        emit SaleSetOrUpdated(_nftType, _tryout, _maxPerWalletSupply, _tiersMaxSupply, _tiersPrice);
    }

    // =======================================================================
    // Deleter functions.
    // =======================================================================

    function deleteSport(StoreGetter.NftType memory _nftType)
        external
        onlyOwner
    {
        StoreDeleter.deleteSport(store, _nftType);
    }

    function deleteLeague(StoreGetter.NftType memory _nftType)
        external
        onlyOwner
    {
        StoreDeleter.deleteLeague(store, _nftType);
    }

    function deleteSeason(StoreGetter.NftType memory _nftType)
        external
        onlyOwner
    {
        StoreDeleter.deleteSeason(store, _nftType);

        // Update state vars.

        store.subcollectionsCnt--;
    }

    function deleteSale(StoreGetter.NftType memory _nftType)
        external
        onlyOwner
    {
        StoreDeleter.deleteSale(store, _nftType);
    }

    // =======================================================================
    // Getter functions.
    // =======================================================================
    // NOTE: Putting getters in a separate library, just as we did for deleters, costs us more in gas, since both the contract and the library get more bloated (at least for the deployment), hence we don't do it.

    function getStore()
        external
        view
        returns(uint256 _unspentOperationalFundAmount, uint16 _subcollectionsCnt, bool _canAutomatorSignFreeMints, string[] memory _sportNames)
    {
        return (store.unspentOperationalFundAmount, store.subcollectionsCnt, store.canAutomatorSignFreeMints, store.hSportNames);
    }

    function getSport(StoreGetter.NftType memory _nftType)
        external
        view
        returns(string[] memory _prizeTypes, string[] memory _leagueNames)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.Sport storage sport = StoreGetter._getSport(store, _nftType);

        // Get state vars.

        return (sport.prizeTypes, sport.hLeagueNames);
    }

    function getLeague(StoreGetter.NftType memory _nftType)
        external
        view
        returns(string[] memory _seasonNames)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.League storage league = StoreGetter._getLeague(store, _nftType);

        // Get state vars.

        return league.hSeasonNames;
    }

    // NOTE: Types (i.e. `struct`) containing (nested or not) mappings can only be parameters or return variables of internal or library functions. Hence we can't directly return the `StoreNode`'s `struct`s.
    function getSeason(StoreGetter.NftType memory _nftType)
        external
        view
        returns(address _nftContract, uint16 _subcollectionIdx, address _revealedRngProvider, uint24[2] memory _maxSupplyAndSold, uint24[2] memory _maxFreeSupplyAndSold, uint24 _mintPeriodMins, uint16[2] memory _prizePoolBpsAndUnlockedBps, uint256[2] memory _totalAndSpentPrizePoolAmount, uint16[] memory _prizeTypePcts, uint24 _prizeClaimPeriodMins, string[] memory _saleNames)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.Season storage season = StoreGetter._getSeason(store, _nftType);

        // Get state vars.

        return (address(season.nftContract), season.subcollectionIdx, season.revealedRngProvider, [season.maxSupply, season.hSold], [season.maxFreeSupply, season.freeSold], season.mintPeriodMins, [season.prizePoolBps, season.unlockedPrizePoolBps], [season.totalPrizePoolAmount, season.spentPrizePoolAmount], season.prizeTypePcts, season.prizeClaimPeriodMins, season.hSaleNames);
    }

    function getSale(StoreGetter.NftType memory _nftType)
        external
        view
        returns(bool _tryout, uint24[2] memory _maxPerWalletSupplyAndMaxSold, uint8 _tiersCnt)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.Sale storage sale = StoreGetter._getSale(store, _nftType);

        // Get state vars.

        return (sale.tryout, [sale.maxPerWalletSupply, sale.hMaxPerWalletSold], uint8(sale.tiers.length));
    }

    function getTier(StoreGetter.NftType memory _nftType)
        external
        view
        returns(uint8 _tokenPresetURIIdx, uint24 _maxSupply, uint24 _unlockedMaxSupply, uint24 _sold, uint256 _price)
    {
        // Validate input params.

        // Validate existence of objects associated with the input params.
        StoreGetter.Tier storage tier = StoreGetter._getTier(store, _nftType);

        // Get state vars.

        return (tier.tokenPresetURIIdx, tier.maxSupply, tier.unlockedMaxSupply, tier.sold, tier.price);
    }

}

// File: contracts/nfts/IAkshunSeasonPassNft.sol



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

// NOTE: We can also use the interfaces for reading/changing the state of already deployed/existing (e.g. others') contracts and libraries. Only include the methods needed/used, because just those methods will be imported and compiled to ABI, so you save gas on contract/library deployment. You create an instance of such interfaced contract/library from their address, preferably by passing it in the constuctor or/and initializer when deploying.
interface IAkshunSeasonPassNft {
    function mint(uint16 subcollectionIdx, uint8 tokenPresetURIIdx, uint8 tokenBaseURIIdx, address to) external returns(uint256);
    function burn(uint256 tokenId) external;
    function ownerOf(uint256 tokenId) external returns(address);
}