ETH Price: $3,344.35 (-4.09%)

Contract Diff Checker

Contract Name:
CollectionLib

Contract Source Code:

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

pragma solidity ^0.8.20;

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

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

pragma solidity ^0.8.20;

/**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {
    /**
     * @dev Muldiv operation overflow.
     */
    error MathOverflowedMulDiv();

    enum Rounding {
        Floor, // Toward negative infinity
        Ceil, // Toward positive infinity
        Trunc, // Toward zero
        Expand // Away from zero
    }

    /**
     * @dev Returns the addition of two unsigned integers, with an overflow flag.
     */
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

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

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

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

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

    /**
     * @dev Returns the 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 towards infinity instead
     * of rounding towards zero.
     */
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        if (b == 0) {
            // Guarantee the same behavior as in a regular Solidity division.
            return a / b;
        }

        // (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 = x * y; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly {
                let mm := mulmod(x, y, not(0))
                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.
            if (denominator <= prod1) {
                revert MathOverflowedMulDiv();
            }

            ///////////////////////////////////////////////
            // 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.

            uint256 twos = denominator & (0 - denominator);
            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 (unsignedRoundsUp(rounding) && 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
     * towards zero.
     *
     * 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 + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 2 of a positive value rounded towards zero.
     * 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 + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 10 of a positive value rounded towards zero.
     * 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 + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 256 of a positive value rounded towards zero.
     * 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 + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);
        }
    }

    /**
     * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
     */
    function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
        return uint8(rounding) % 2 == 1;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SafeCast.sol)
// This file was procedurally generated from scripts/generate/templates/SafeCast.js.

pragma solidity ^0.8.20;

/**
 * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow
 * checks.
 *
 * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
 * easily result in undesired exploitation or bugs, since developers usually
 * assume that overflows raise errors. `SafeCast` restores this intuition by
 * reverting the transaction when such an operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeCast {
    /**
     * @dev Value doesn't fit in an uint of `bits` size.
     */
    error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);

    /**
     * @dev An int value doesn't fit in an uint of `bits` size.
     */
    error SafeCastOverflowedIntToUint(int256 value);

    /**
     * @dev Value doesn't fit in an int of `bits` size.
     */
    error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);

    /**
     * @dev An uint value doesn't fit in an int of `bits` size.
     */
    error SafeCastOverflowedUintToInt(uint256 value);

    /**
     * @dev Returns the downcasted uint248 from uint256, reverting on
     * overflow (when the input is greater than largest uint248).
     *
     * Counterpart to Solidity's `uint248` operator.
     *
     * Requirements:
     *
     * - input must fit into 248 bits
     */
    function toUint248(uint256 value) internal pure returns (uint248) {
        if (value > type(uint248).max) {
            revert SafeCastOverflowedUintDowncast(248, value);
        }
        return uint248(value);
    }

    /**
     * @dev Returns the downcasted uint240 from uint256, reverting on
     * overflow (when the input is greater than largest uint240).
     *
     * Counterpart to Solidity's `uint240` operator.
     *
     * Requirements:
     *
     * - input must fit into 240 bits
     */
    function toUint240(uint256 value) internal pure returns (uint240) {
        if (value > type(uint240).max) {
            revert SafeCastOverflowedUintDowncast(240, value);
        }
        return uint240(value);
    }

    /**
     * @dev Returns the downcasted uint232 from uint256, reverting on
     * overflow (when the input is greater than largest uint232).
     *
     * Counterpart to Solidity's `uint232` operator.
     *
     * Requirements:
     *
     * - input must fit into 232 bits
     */
    function toUint232(uint256 value) internal pure returns (uint232) {
        if (value > type(uint232).max) {
            revert SafeCastOverflowedUintDowncast(232, value);
        }
        return uint232(value);
    }

    /**
     * @dev Returns the downcasted uint224 from uint256, reverting on
     * overflow (when the input is greater than largest uint224).
     *
     * Counterpart to Solidity's `uint224` operator.
     *
     * Requirements:
     *
     * - input must fit into 224 bits
     */
    function toUint224(uint256 value) internal pure returns (uint224) {
        if (value > type(uint224).max) {
            revert SafeCastOverflowedUintDowncast(224, value);
        }
        return uint224(value);
    }

    /**
     * @dev Returns the downcasted uint216 from uint256, reverting on
     * overflow (when the input is greater than largest uint216).
     *
     * Counterpart to Solidity's `uint216` operator.
     *
     * Requirements:
     *
     * - input must fit into 216 bits
     */
    function toUint216(uint256 value) internal pure returns (uint216) {
        if (value > type(uint216).max) {
            revert SafeCastOverflowedUintDowncast(216, value);
        }
        return uint216(value);
    }

    /**
     * @dev Returns the downcasted uint208 from uint256, reverting on
     * overflow (when the input is greater than largest uint208).
     *
     * Counterpart to Solidity's `uint208` operator.
     *
     * Requirements:
     *
     * - input must fit into 208 bits
     */
    function toUint208(uint256 value) internal pure returns (uint208) {
        if (value > type(uint208).max) {
            revert SafeCastOverflowedUintDowncast(208, value);
        }
        return uint208(value);
    }

    /**
     * @dev Returns the downcasted uint200 from uint256, reverting on
     * overflow (when the input is greater than largest uint200).
     *
     * Counterpart to Solidity's `uint200` operator.
     *
     * Requirements:
     *
     * - input must fit into 200 bits
     */
    function toUint200(uint256 value) internal pure returns (uint200) {
        if (value > type(uint200).max) {
            revert SafeCastOverflowedUintDowncast(200, value);
        }
        return uint200(value);
    }

    /**
     * @dev Returns the downcasted uint192 from uint256, reverting on
     * overflow (when the input is greater than largest uint192).
     *
     * Counterpart to Solidity's `uint192` operator.
     *
     * Requirements:
     *
     * - input must fit into 192 bits
     */
    function toUint192(uint256 value) internal pure returns (uint192) {
        if (value > type(uint192).max) {
            revert SafeCastOverflowedUintDowncast(192, value);
        }
        return uint192(value);
    }

    /**
     * @dev Returns the downcasted uint184 from uint256, reverting on
     * overflow (when the input is greater than largest uint184).
     *
     * Counterpart to Solidity's `uint184` operator.
     *
     * Requirements:
     *
     * - input must fit into 184 bits
     */
    function toUint184(uint256 value) internal pure returns (uint184) {
        if (value > type(uint184).max) {
            revert SafeCastOverflowedUintDowncast(184, value);
        }
        return uint184(value);
    }

    /**
     * @dev Returns the downcasted uint176 from uint256, reverting on
     * overflow (when the input is greater than largest uint176).
     *
     * Counterpart to Solidity's `uint176` operator.
     *
     * Requirements:
     *
     * - input must fit into 176 bits
     */
    function toUint176(uint256 value) internal pure returns (uint176) {
        if (value > type(uint176).max) {
            revert SafeCastOverflowedUintDowncast(176, value);
        }
        return uint176(value);
    }

    /**
     * @dev Returns the downcasted uint168 from uint256, reverting on
     * overflow (when the input is greater than largest uint168).
     *
     * Counterpart to Solidity's `uint168` operator.
     *
     * Requirements:
     *
     * - input must fit into 168 bits
     */
    function toUint168(uint256 value) internal pure returns (uint168) {
        if (value > type(uint168).max) {
            revert SafeCastOverflowedUintDowncast(168, value);
        }
        return uint168(value);
    }

    /**
     * @dev Returns the downcasted uint160 from uint256, reverting on
     * overflow (when the input is greater than largest uint160).
     *
     * Counterpart to Solidity's `uint160` operator.
     *
     * Requirements:
     *
     * - input must fit into 160 bits
     */
    function toUint160(uint256 value) internal pure returns (uint160) {
        if (value > type(uint160).max) {
            revert SafeCastOverflowedUintDowncast(160, value);
        }
        return uint160(value);
    }

    /**
     * @dev Returns the downcasted uint152 from uint256, reverting on
     * overflow (when the input is greater than largest uint152).
     *
     * Counterpart to Solidity's `uint152` operator.
     *
     * Requirements:
     *
     * - input must fit into 152 bits
     */
    function toUint152(uint256 value) internal pure returns (uint152) {
        if (value > type(uint152).max) {
            revert SafeCastOverflowedUintDowncast(152, value);
        }
        return uint152(value);
    }

    /**
     * @dev Returns the downcasted uint144 from uint256, reverting on
     * overflow (when the input is greater than largest uint144).
     *
     * Counterpart to Solidity's `uint144` operator.
     *
     * Requirements:
     *
     * - input must fit into 144 bits
     */
    function toUint144(uint256 value) internal pure returns (uint144) {
        if (value > type(uint144).max) {
            revert SafeCastOverflowedUintDowncast(144, value);
        }
        return uint144(value);
    }

    /**
     * @dev Returns the downcasted uint136 from uint256, reverting on
     * overflow (when the input is greater than largest uint136).
     *
     * Counterpart to Solidity's `uint136` operator.
     *
     * Requirements:
     *
     * - input must fit into 136 bits
     */
    function toUint136(uint256 value) internal pure returns (uint136) {
        if (value > type(uint136).max) {
            revert SafeCastOverflowedUintDowncast(136, value);
        }
        return uint136(value);
    }

    /**
     * @dev Returns the downcasted uint128 from uint256, reverting on
     * overflow (when the input is greater than largest uint128).
     *
     * Counterpart to Solidity's `uint128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     */
    function toUint128(uint256 value) internal pure returns (uint128) {
        if (value > type(uint128).max) {
            revert SafeCastOverflowedUintDowncast(128, value);
        }
        return uint128(value);
    }

    /**
     * @dev Returns the downcasted uint120 from uint256, reverting on
     * overflow (when the input is greater than largest uint120).
     *
     * Counterpart to Solidity's `uint120` operator.
     *
     * Requirements:
     *
     * - input must fit into 120 bits
     */
    function toUint120(uint256 value) internal pure returns (uint120) {
        if (value > type(uint120).max) {
            revert SafeCastOverflowedUintDowncast(120, value);
        }
        return uint120(value);
    }

    /**
     * @dev Returns the downcasted uint112 from uint256, reverting on
     * overflow (when the input is greater than largest uint112).
     *
     * Counterpart to Solidity's `uint112` operator.
     *
     * Requirements:
     *
     * - input must fit into 112 bits
     */
    function toUint112(uint256 value) internal pure returns (uint112) {
        if (value > type(uint112).max) {
            revert SafeCastOverflowedUintDowncast(112, value);
        }
        return uint112(value);
    }

    /**
     * @dev Returns the downcasted uint104 from uint256, reverting on
     * overflow (when the input is greater than largest uint104).
     *
     * Counterpart to Solidity's `uint104` operator.
     *
     * Requirements:
     *
     * - input must fit into 104 bits
     */
    function toUint104(uint256 value) internal pure returns (uint104) {
        if (value > type(uint104).max) {
            revert SafeCastOverflowedUintDowncast(104, value);
        }
        return uint104(value);
    }

    /**
     * @dev Returns the downcasted uint96 from uint256, reverting on
     * overflow (when the input is greater than largest uint96).
     *
     * Counterpart to Solidity's `uint96` operator.
     *
     * Requirements:
     *
     * - input must fit into 96 bits
     */
    function toUint96(uint256 value) internal pure returns (uint96) {
        if (value > type(uint96).max) {
            revert SafeCastOverflowedUintDowncast(96, value);
        }
        return uint96(value);
    }

    /**
     * @dev Returns the downcasted uint88 from uint256, reverting on
     * overflow (when the input is greater than largest uint88).
     *
     * Counterpart to Solidity's `uint88` operator.
     *
     * Requirements:
     *
     * - input must fit into 88 bits
     */
    function toUint88(uint256 value) internal pure returns (uint88) {
        if (value > type(uint88).max) {
            revert SafeCastOverflowedUintDowncast(88, value);
        }
        return uint88(value);
    }

    /**
     * @dev Returns the downcasted uint80 from uint256, reverting on
     * overflow (when the input is greater than largest uint80).
     *
     * Counterpart to Solidity's `uint80` operator.
     *
     * Requirements:
     *
     * - input must fit into 80 bits
     */
    function toUint80(uint256 value) internal pure returns (uint80) {
        if (value > type(uint80).max) {
            revert SafeCastOverflowedUintDowncast(80, value);
        }
        return uint80(value);
    }

    /**
     * @dev Returns the downcasted uint72 from uint256, reverting on
     * overflow (when the input is greater than largest uint72).
     *
     * Counterpart to Solidity's `uint72` operator.
     *
     * Requirements:
     *
     * - input must fit into 72 bits
     */
    function toUint72(uint256 value) internal pure returns (uint72) {
        if (value > type(uint72).max) {
            revert SafeCastOverflowedUintDowncast(72, value);
        }
        return uint72(value);
    }

    /**
     * @dev Returns the downcasted uint64 from uint256, reverting on
     * overflow (when the input is greater than largest uint64).
     *
     * Counterpart to Solidity's `uint64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     */
    function toUint64(uint256 value) internal pure returns (uint64) {
        if (value > type(uint64).max) {
            revert SafeCastOverflowedUintDowncast(64, value);
        }
        return uint64(value);
    }

    /**
     * @dev Returns the downcasted uint56 from uint256, reverting on
     * overflow (when the input is greater than largest uint56).
     *
     * Counterpart to Solidity's `uint56` operator.
     *
     * Requirements:
     *
     * - input must fit into 56 bits
     */
    function toUint56(uint256 value) internal pure returns (uint56) {
        if (value > type(uint56).max) {
            revert SafeCastOverflowedUintDowncast(56, value);
        }
        return uint56(value);
    }

    /**
     * @dev Returns the downcasted uint48 from uint256, reverting on
     * overflow (when the input is greater than largest uint48).
     *
     * Counterpart to Solidity's `uint48` operator.
     *
     * Requirements:
     *
     * - input must fit into 48 bits
     */
    function toUint48(uint256 value) internal pure returns (uint48) {
        if (value > type(uint48).max) {
            revert SafeCastOverflowedUintDowncast(48, value);
        }
        return uint48(value);
    }

    /**
     * @dev Returns the downcasted uint40 from uint256, reverting on
     * overflow (when the input is greater than largest uint40).
     *
     * Counterpart to Solidity's `uint40` operator.
     *
     * Requirements:
     *
     * - input must fit into 40 bits
     */
    function toUint40(uint256 value) internal pure returns (uint40) {
        if (value > type(uint40).max) {
            revert SafeCastOverflowedUintDowncast(40, value);
        }
        return uint40(value);
    }

    /**
     * @dev Returns the downcasted uint32 from uint256, reverting on
     * overflow (when the input is greater than largest uint32).
     *
     * Counterpart to Solidity's `uint32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     */
    function toUint32(uint256 value) internal pure returns (uint32) {
        if (value > type(uint32).max) {
            revert SafeCastOverflowedUintDowncast(32, value);
        }
        return uint32(value);
    }

    /**
     * @dev Returns the downcasted uint24 from uint256, reverting on
     * overflow (when the input is greater than largest uint24).
     *
     * Counterpart to Solidity's `uint24` operator.
     *
     * Requirements:
     *
     * - input must fit into 24 bits
     */
    function toUint24(uint256 value) internal pure returns (uint24) {
        if (value > type(uint24).max) {
            revert SafeCastOverflowedUintDowncast(24, value);
        }
        return uint24(value);
    }

    /**
     * @dev Returns the downcasted uint16 from uint256, reverting on
     * overflow (when the input is greater than largest uint16).
     *
     * Counterpart to Solidity's `uint16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     */
    function toUint16(uint256 value) internal pure returns (uint16) {
        if (value > type(uint16).max) {
            revert SafeCastOverflowedUintDowncast(16, value);
        }
        return uint16(value);
    }

    /**
     * @dev Returns the downcasted uint8 from uint256, reverting on
     * overflow (when the input is greater than largest uint8).
     *
     * Counterpart to Solidity's `uint8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits
     */
    function toUint8(uint256 value) internal pure returns (uint8) {
        if (value > type(uint8).max) {
            revert SafeCastOverflowedUintDowncast(8, value);
        }
        return uint8(value);
    }

    /**
     * @dev Converts a signed int256 into an unsigned uint256.
     *
     * Requirements:
     *
     * - input must be greater than or equal to 0.
     */
    function toUint256(int256 value) internal pure returns (uint256) {
        if (value < 0) {
            revert SafeCastOverflowedIntToUint(value);
        }
        return uint256(value);
    }

    /**
     * @dev Returns the downcasted int248 from int256, reverting on
     * overflow (when the input is less than smallest int248 or
     * greater than largest int248).
     *
     * Counterpart to Solidity's `int248` operator.
     *
     * Requirements:
     *
     * - input must fit into 248 bits
     */
    function toInt248(int256 value) internal pure returns (int248 downcasted) {
        downcasted = int248(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(248, value);
        }
    }

    /**
     * @dev Returns the downcasted int240 from int256, reverting on
     * overflow (when the input is less than smallest int240 or
     * greater than largest int240).
     *
     * Counterpart to Solidity's `int240` operator.
     *
     * Requirements:
     *
     * - input must fit into 240 bits
     */
    function toInt240(int256 value) internal pure returns (int240 downcasted) {
        downcasted = int240(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(240, value);
        }
    }

    /**
     * @dev Returns the downcasted int232 from int256, reverting on
     * overflow (when the input is less than smallest int232 or
     * greater than largest int232).
     *
     * Counterpart to Solidity's `int232` operator.
     *
     * Requirements:
     *
     * - input must fit into 232 bits
     */
    function toInt232(int256 value) internal pure returns (int232 downcasted) {
        downcasted = int232(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(232, value);
        }
    }

    /**
     * @dev Returns the downcasted int224 from int256, reverting on
     * overflow (when the input is less than smallest int224 or
     * greater than largest int224).
     *
     * Counterpart to Solidity's `int224` operator.
     *
     * Requirements:
     *
     * - input must fit into 224 bits
     */
    function toInt224(int256 value) internal pure returns (int224 downcasted) {
        downcasted = int224(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(224, value);
        }
    }

    /**
     * @dev Returns the downcasted int216 from int256, reverting on
     * overflow (when the input is less than smallest int216 or
     * greater than largest int216).
     *
     * Counterpart to Solidity's `int216` operator.
     *
     * Requirements:
     *
     * - input must fit into 216 bits
     */
    function toInt216(int256 value) internal pure returns (int216 downcasted) {
        downcasted = int216(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(216, value);
        }
    }

    /**
     * @dev Returns the downcasted int208 from int256, reverting on
     * overflow (when the input is less than smallest int208 or
     * greater than largest int208).
     *
     * Counterpart to Solidity's `int208` operator.
     *
     * Requirements:
     *
     * - input must fit into 208 bits
     */
    function toInt208(int256 value) internal pure returns (int208 downcasted) {
        downcasted = int208(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(208, value);
        }
    }

    /**
     * @dev Returns the downcasted int200 from int256, reverting on
     * overflow (when the input is less than smallest int200 or
     * greater than largest int200).
     *
     * Counterpart to Solidity's `int200` operator.
     *
     * Requirements:
     *
     * - input must fit into 200 bits
     */
    function toInt200(int256 value) internal pure returns (int200 downcasted) {
        downcasted = int200(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(200, value);
        }
    }

    /**
     * @dev Returns the downcasted int192 from int256, reverting on
     * overflow (when the input is less than smallest int192 or
     * greater than largest int192).
     *
     * Counterpart to Solidity's `int192` operator.
     *
     * Requirements:
     *
     * - input must fit into 192 bits
     */
    function toInt192(int256 value) internal pure returns (int192 downcasted) {
        downcasted = int192(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(192, value);
        }
    }

    /**
     * @dev Returns the downcasted int184 from int256, reverting on
     * overflow (when the input is less than smallest int184 or
     * greater than largest int184).
     *
     * Counterpart to Solidity's `int184` operator.
     *
     * Requirements:
     *
     * - input must fit into 184 bits
     */
    function toInt184(int256 value) internal pure returns (int184 downcasted) {
        downcasted = int184(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(184, value);
        }
    }

    /**
     * @dev Returns the downcasted int176 from int256, reverting on
     * overflow (when the input is less than smallest int176 or
     * greater than largest int176).
     *
     * Counterpart to Solidity's `int176` operator.
     *
     * Requirements:
     *
     * - input must fit into 176 bits
     */
    function toInt176(int256 value) internal pure returns (int176 downcasted) {
        downcasted = int176(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(176, value);
        }
    }

    /**
     * @dev Returns the downcasted int168 from int256, reverting on
     * overflow (when the input is less than smallest int168 or
     * greater than largest int168).
     *
     * Counterpart to Solidity's `int168` operator.
     *
     * Requirements:
     *
     * - input must fit into 168 bits
     */
    function toInt168(int256 value) internal pure returns (int168 downcasted) {
        downcasted = int168(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(168, value);
        }
    }

    /**
     * @dev Returns the downcasted int160 from int256, reverting on
     * overflow (when the input is less than smallest int160 or
     * greater than largest int160).
     *
     * Counterpart to Solidity's `int160` operator.
     *
     * Requirements:
     *
     * - input must fit into 160 bits
     */
    function toInt160(int256 value) internal pure returns (int160 downcasted) {
        downcasted = int160(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(160, value);
        }
    }

    /**
     * @dev Returns the downcasted int152 from int256, reverting on
     * overflow (when the input is less than smallest int152 or
     * greater than largest int152).
     *
     * Counterpart to Solidity's `int152` operator.
     *
     * Requirements:
     *
     * - input must fit into 152 bits
     */
    function toInt152(int256 value) internal pure returns (int152 downcasted) {
        downcasted = int152(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(152, value);
        }
    }

    /**
     * @dev Returns the downcasted int144 from int256, reverting on
     * overflow (when the input is less than smallest int144 or
     * greater than largest int144).
     *
     * Counterpart to Solidity's `int144` operator.
     *
     * Requirements:
     *
     * - input must fit into 144 bits
     */
    function toInt144(int256 value) internal pure returns (int144 downcasted) {
        downcasted = int144(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(144, value);
        }
    }

    /**
     * @dev Returns the downcasted int136 from int256, reverting on
     * overflow (when the input is less than smallest int136 or
     * greater than largest int136).
     *
     * Counterpart to Solidity's `int136` operator.
     *
     * Requirements:
     *
     * - input must fit into 136 bits
     */
    function toInt136(int256 value) internal pure returns (int136 downcasted) {
        downcasted = int136(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(136, value);
        }
    }

    /**
     * @dev Returns the downcasted int128 from int256, reverting on
     * overflow (when the input is less than smallest int128 or
     * greater than largest int128).
     *
     * Counterpart to Solidity's `int128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     */
    function toInt128(int256 value) internal pure returns (int128 downcasted) {
        downcasted = int128(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(128, value);
        }
    }

    /**
     * @dev Returns the downcasted int120 from int256, reverting on
     * overflow (when the input is less than smallest int120 or
     * greater than largest int120).
     *
     * Counterpart to Solidity's `int120` operator.
     *
     * Requirements:
     *
     * - input must fit into 120 bits
     */
    function toInt120(int256 value) internal pure returns (int120 downcasted) {
        downcasted = int120(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(120, value);
        }
    }

    /**
     * @dev Returns the downcasted int112 from int256, reverting on
     * overflow (when the input is less than smallest int112 or
     * greater than largest int112).
     *
     * Counterpart to Solidity's `int112` operator.
     *
     * Requirements:
     *
     * - input must fit into 112 bits
     */
    function toInt112(int256 value) internal pure returns (int112 downcasted) {
        downcasted = int112(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(112, value);
        }
    }

    /**
     * @dev Returns the downcasted int104 from int256, reverting on
     * overflow (when the input is less than smallest int104 or
     * greater than largest int104).
     *
     * Counterpart to Solidity's `int104` operator.
     *
     * Requirements:
     *
     * - input must fit into 104 bits
     */
    function toInt104(int256 value) internal pure returns (int104 downcasted) {
        downcasted = int104(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(104, value);
        }
    }

    /**
     * @dev Returns the downcasted int96 from int256, reverting on
     * overflow (when the input is less than smallest int96 or
     * greater than largest int96).
     *
     * Counterpart to Solidity's `int96` operator.
     *
     * Requirements:
     *
     * - input must fit into 96 bits
     */
    function toInt96(int256 value) internal pure returns (int96 downcasted) {
        downcasted = int96(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(96, value);
        }
    }

    /**
     * @dev Returns the downcasted int88 from int256, reverting on
     * overflow (when the input is less than smallest int88 or
     * greater than largest int88).
     *
     * Counterpart to Solidity's `int88` operator.
     *
     * Requirements:
     *
     * - input must fit into 88 bits
     */
    function toInt88(int256 value) internal pure returns (int88 downcasted) {
        downcasted = int88(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(88, value);
        }
    }

    /**
     * @dev Returns the downcasted int80 from int256, reverting on
     * overflow (when the input is less than smallest int80 or
     * greater than largest int80).
     *
     * Counterpart to Solidity's `int80` operator.
     *
     * Requirements:
     *
     * - input must fit into 80 bits
     */
    function toInt80(int256 value) internal pure returns (int80 downcasted) {
        downcasted = int80(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(80, value);
        }
    }

    /**
     * @dev Returns the downcasted int72 from int256, reverting on
     * overflow (when the input is less than smallest int72 or
     * greater than largest int72).
     *
     * Counterpart to Solidity's `int72` operator.
     *
     * Requirements:
     *
     * - input must fit into 72 bits
     */
    function toInt72(int256 value) internal pure returns (int72 downcasted) {
        downcasted = int72(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(72, value);
        }
    }

    /**
     * @dev Returns the downcasted int64 from int256, reverting on
     * overflow (when the input is less than smallest int64 or
     * greater than largest int64).
     *
     * Counterpart to Solidity's `int64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     */
    function toInt64(int256 value) internal pure returns (int64 downcasted) {
        downcasted = int64(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(64, value);
        }
    }

    /**
     * @dev Returns the downcasted int56 from int256, reverting on
     * overflow (when the input is less than smallest int56 or
     * greater than largest int56).
     *
     * Counterpart to Solidity's `int56` operator.
     *
     * Requirements:
     *
     * - input must fit into 56 bits
     */
    function toInt56(int256 value) internal pure returns (int56 downcasted) {
        downcasted = int56(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(56, value);
        }
    }

    /**
     * @dev Returns the downcasted int48 from int256, reverting on
     * overflow (when the input is less than smallest int48 or
     * greater than largest int48).
     *
     * Counterpart to Solidity's `int48` operator.
     *
     * Requirements:
     *
     * - input must fit into 48 bits
     */
    function toInt48(int256 value) internal pure returns (int48 downcasted) {
        downcasted = int48(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(48, value);
        }
    }

    /**
     * @dev Returns the downcasted int40 from int256, reverting on
     * overflow (when the input is less than smallest int40 or
     * greater than largest int40).
     *
     * Counterpart to Solidity's `int40` operator.
     *
     * Requirements:
     *
     * - input must fit into 40 bits
     */
    function toInt40(int256 value) internal pure returns (int40 downcasted) {
        downcasted = int40(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(40, value);
        }
    }

    /**
     * @dev Returns the downcasted int32 from int256, reverting on
     * overflow (when the input is less than smallest int32 or
     * greater than largest int32).
     *
     * Counterpart to Solidity's `int32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     */
    function toInt32(int256 value) internal pure returns (int32 downcasted) {
        downcasted = int32(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(32, value);
        }
    }

    /**
     * @dev Returns the downcasted int24 from int256, reverting on
     * overflow (when the input is less than smallest int24 or
     * greater than largest int24).
     *
     * Counterpart to Solidity's `int24` operator.
     *
     * Requirements:
     *
     * - input must fit into 24 bits
     */
    function toInt24(int256 value) internal pure returns (int24 downcasted) {
        downcasted = int24(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(24, value);
        }
    }

    /**
     * @dev Returns the downcasted int16 from int256, reverting on
     * overflow (when the input is less than smallest int16 or
     * greater than largest int16).
     *
     * Counterpart to Solidity's `int16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     */
    function toInt16(int256 value) internal pure returns (int16 downcasted) {
        downcasted = int16(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(16, value);
        }
    }

    /**
     * @dev Returns the downcasted int8 from int256, reverting on
     * overflow (when the input is less than smallest int8 or
     * greater than largest int8).
     *
     * Counterpart to Solidity's `int8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits
     */
    function toInt8(int256 value) internal pure returns (int8 downcasted) {
        downcasted = int8(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(8, value);
        }
    }

    /**
     * @dev Converts an unsigned uint256 into a signed int256.
     *
     * Requirements:
     *
     * - input must be less than or equal to maxInt256.
     */
    function toInt256(uint256 value) internal pure returns (int256) {
        // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
        if (value > uint256(type(int256).max)) {
            revert SafeCastOverflowedUintToInt(value);
        }
        return int256(value);
    }
}

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

library Constants {
    /// @notice Flooring protocol
    /// @dev floor token amount of 1 NFT (with 18 decimals)
    uint256 public constant FLOOR_TOKEN_AMOUNT = 1_000_000 ether;
    /// @dev The minimum vip level required to use `proxy collection`
    uint8 public constant PROXY_COLLECTION_VIP_THRESHOLD = 3;

    /// @notice Rolling Bucket Constant Conf
    uint256 public constant BUCKET_SPAN_1 = 259199 seconds; // BUCKET_SPAN minus 1, used for rounding up
    uint256 public constant BUCKET_SPAN = 3 days;
    uint256 public constant MAX_LOCKING_BUCKET = 240;
    uint256 public constant MAX_LOCKING_PERIOD = 720 days; // MAX LOCKING BUCKET * BUCKET_SPAN

    /// @notice Auction Config
    uint256 public constant FREE_AUCTION_PERIOD = 24 hours;
    uint256 public constant AUCTION_INITIAL_PERIODS = 24 hours;
    uint256 public constant AUCTION_COMPLETE_GRACE_PERIODS = 2 days;
    /// @dev minimum bid per NFT when someone starts aution on expired safebox
    uint256 public constant AUCTION_ON_EXPIRED_MINIMUM_BID = 100 ether;
    /// @dev admin fee charged per NFT when someone starts aution on expired safebox
    uint256 public constant AUCTION_ON_EXPIRED_SAFEBOX_COST = 100 ether;
    /// @dev admin fee charged per NFT when owner starts aution on himself safebox
    uint256 public constant AUCTION_COST = 100 ether;

    /// @notice Raffle Config
    uint256 public constant RAFFLE_COST = 500 ether;
    uint256 public constant RAFFLE_COMPLETE_GRACE_PERIODS = 2 days;

    /// @notice Private offer Config
    uint256 public constant PRIVATE_OFFER_DURATION = 24 hours;
    uint256 public constant PRIVATE_OFFER_COMPLETE_GRACE_DURATION = 2 days;
    uint256 public constant PRIVATE_OFFER_COST = 0;

    uint256 public constant ADD_FREE_NFT_REWARD = 0;

    /// @notice Lock/Unlock config
    uint256 public constant LOCKING_RAMP_UP_THRESHOLD = 20;

    uint256 public constant LOCKING_PCT_TO_SAFEBOX_MAINT_MIN = 1200 ether;
    uint256 public constant LOCKING_PCT_TO_SAFEBOX_MAINT_MAX = 345600 ether;

    /// @notice Activities Fee Rate

    /// @notice Fee rate used to distribute funds that collected from Auctions on expired safeboxes.
    /// these auction would be settled using credit token
    uint256 public constant FREE_AUCTION_FEE_RATE_BIPS = 2000; // 20%
    /// @notice Fee rate settled with credit token
    uint256 public constant CREDIT_FEE_RATE_BIPS = 150; // 2%
    /// @notice Fee rate settled with specified token
    uint256 public constant SPEC_FEE_RATE_BIPS = 300; // 3%
    /// @notice Fee rate settled with all other tokens
    uint256 public constant COMMON_FEE_RATE_BIPS = 500; // 5%

    uint256 public constant VIP_LEVEL_COUNT = 8;

    struct AuctionBidOption {
        uint256 extendDurationSecs;
        uint256 minimumRaisePct;
        uint256 vipLevel;
    }

    function getVipLockingBuckets(uint256 vipLevel) internal pure returns (uint256 buckets) {
        require(vipLevel < VIP_LEVEL_COUNT);
        assembly {
            switch vipLevel
            case 1 { buckets := 1 }
            case 2 { buckets := 5 }
            case 3 { buckets := 20 }
            case 4 { buckets := 60 }
            case 5 { buckets := 120 }
            case 6 { buckets := 180 }
            case 7 { buckets := MAX_LOCKING_BUCKET }
        }
    }

    function getVipLevel(uint256 totalCredit) internal pure returns (uint8) {
        if (totalCredit < 30_000 ether) {
            return 0;
        } else if (totalCredit < 100_000 ether) {
            return 1;
        } else if (totalCredit < 300_000 ether) {
            return 2;
        } else if (totalCredit < 1_000_000 ether) {
            return 3;
        } else if (totalCredit < 3_000_000 ether) {
            return 4;
        } else if (totalCredit < 10_000_000 ether) {
            return 5;
        } else if (totalCredit < 30_000_000 ether) {
            return 6;
        } else {
            return 7;
        }
    }

    function getVipBalanceRequirements(uint256 vipLevel) internal pure returns (uint256 required) {
        require(vipLevel < VIP_LEVEL_COUNT);

        assembly {
            switch vipLevel
            case 1 { required := 30000 }
            case 2 { required := 100000 }
            case 3 { required := 300000 }
            case 4 { required := 1000000 }
            case 5 { required := 3000000 }
            case 6 { required := 10000000 }
            case 7 { required := 30000000 }
        }

        /// credit token should be scaled with 18 decimals(1 ether == 10**18)
        unchecked {
            return required * 1 ether;
        }
    }

    function getBidOption(uint256 idx) internal pure returns (AuctionBidOption memory) {
        require(idx < 4);
        AuctionBidOption[4] memory bidOptions = [
            AuctionBidOption({extendDurationSecs: 5 minutes, minimumRaisePct: 1, vipLevel: 0}),
            AuctionBidOption({extendDurationSecs: 8 hours, minimumRaisePct: 10, vipLevel: 3}),
            AuctionBidOption({extendDurationSecs: 16 hours, minimumRaisePct: 20, vipLevel: 5}),
            AuctionBidOption({extendDurationSecs: 24 hours, minimumRaisePct: 40, vipLevel: 7})
        ];
        return bidOptions[idx];
    }

    function raffleDurations(uint256 idx) internal pure returns (uint256 vipLevel, uint256 duration) {
        require(idx < 6);

        vipLevel = idx;
        assembly {
            switch idx
            case 1 { duration := 1 }
            case 2 { duration := 2 }
            case 3 { duration := 3 }
            case 4 { duration := 5 }
            case 5 { duration := 7 }
        }
        unchecked {
            duration *= 1 days;
        }
    }

    /// return locking ratio restrictions indicates that the vipLevel can utility infinite lock NFTs at corresponding ratio
    function getLockingRatioForInfinite(uint8 vipLevel) internal pure returns (uint256 ratio) {
        assembly {
            switch vipLevel
            case 1 { ratio := 0 }
            case 2 { ratio := 0 }
            case 3 { ratio := 20 }
            case 4 { ratio := 30 }
            case 5 { ratio := 40 }
            case 6 { ratio := 50 }
            case 7 { ratio := 80 }
        }
    }

    function getVipRequiredStakingWithDiscount(uint256 requiredStaking, uint8 vipLevel)
        internal
        pure
        returns (uint256)
    {
        if (vipLevel < 3) {
            return requiredStaking;
        }
        unchecked {
            /// the higher vip level, more discount for staking
            ///  discount range: 10% - 50%
            return requiredStaking * (100 - (vipLevel - 2) * 10) / 100;
        }
    }

    function getRequiredStakingForLockRatio(uint256 locked, uint256 totalManaged) internal pure returns (uint256) {
        if (totalManaged < Constants.LOCKING_RAMP_UP_THRESHOLD) {
            return 1200 ether;
        }

        unchecked {
            uint256 lockingRatioPct = locked * 100 / totalManaged;
            if (lockingRatioPct <= 40) {
                return 1200 ether;
            } else if (lockingRatioPct < 60) {
                return 1320 ether + ((lockingRatioPct - 40) >> 1) * 120 ether;
            } else if (lockingRatioPct < 70) {
                return 2640 ether + ((lockingRatioPct - 60) >> 1) * 240 ether;
            } else if (lockingRatioPct < 80) {
                return 4080 ether + ((lockingRatioPct - 70) >> 1) * 480 ether;
            } else if (lockingRatioPct < 90) {
                return 6960 ether + ((lockingRatioPct - 80) >> 1) * 960 ether;
            } else if (lockingRatioPct < 100) {
                /// 108000 * 2^x
                return (108000 ether << ((lockingRatioPct - 90) >> 1)) / 5;
            } else {
                return 345600 ether;
            }
        }
    }

    function getVipClaimCostWithDiscount(uint256 cost, uint8 vipLevel) internal pure returns (uint256) {
        if (vipLevel < 3) {
            return cost;
        }

        unchecked {
            uint256 discount = 4000 ether << (vipLevel - 3);
            if (cost < discount) {
                return 0;
            } else {
                return cost - discount;
            }
        }
    }

    function getClaimExpiredCost(uint256 locked, uint256 totalManaged, uint8 vipLevel)
        internal
        pure
        returns (uint256)
    {
        uint256 realCost = getClaimCost(locked, totalManaged);
        return getVipClaimCostWithDiscount(realCost, vipLevel);
    }

    function getClaimRandomCost(uint256 locked, uint256 totalManaged, uint8 vipLevel) internal pure returns (uint256) {
        uint256 realCost = getClaimCost(locked, totalManaged);
        return getVipClaimCostWithDiscount(realCost, vipLevel);
    }

    function getClaimCost(uint256 locked, uint256 totalManaged) private pure returns (uint256) {
        if (totalManaged < Constants.LOCKING_RAMP_UP_THRESHOLD) {
            return 0;
        }

        unchecked {
            uint256 lockingRatioPct = locked * 100 / totalManaged;

            if (lockingRatioPct <= 60) {
                return 0;
            } else if (lockingRatioPct < 100) {
                uint256 cost = 1 ether;
                assembly {
                    switch shr(1, sub(lockingRatioPct, 60))
                    case 0 { cost := mul(cost, 400) }
                    case 1 { cost := mul(cost, 600) }
                    case 2 { cost := mul(cost, 800) }
                    case 3 { cost := mul(cost, 1200) }
                    case 4 { cost := mul(cost, 1600) }
                    case 5 { cost := mul(cost, 2400) }
                    case 6 { cost := mul(cost, 3200) }
                    case 7 { cost := mul(cost, 4800) }
                    case 8 { cost := mul(cost, 6400) }
                    case 9 { cost := mul(cost, 9600) }
                    case 10 { cost := mul(cost, 12800) }
                    case 11 { cost := mul(cost, 19200) }
                    case 12 { cost := mul(cost, 25600) }
                    case 13 { cost := mul(cost, 38400) }
                    case 14 { cost := mul(cost, 51200) }
                    case 15 { cost := mul(cost, 76800) }
                    case 16 { cost := mul(cost, 102400) }
                    case 17 { cost := mul(cost, 153600) }
                    case 18 { cost := mul(cost, 204800) }
                    case 19 { cost := mul(cost, 307200) }
                }
                return cost;
            } else {
                return 307200 ether;
            }
        }
    }
}

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

library Errors {
    /// @notice Safe Box error
    error SafeBoxHasExpire();
    error SafeBoxNotExist();
    error SafeBoxHasNotExpire();
    error SafeBoxAlreadyExist();
    error NoMatchingSafeBoxKey();
    error SafeBoxKeyAlreadyExist();

    /// @notice Auction error
    error AuctionHasNotCompleted();
    error AuctionHasExpire();
    error AuctionBidIsNotHighEnough();
    error AuctionBidTokenMismatch();
    error AuctionSelfBid();
    error AuctionInvalidBidAmount();
    error AuctionNotExist();
    error SafeBoxAuctionWindowHasPassed();

    /// @notice Activity common error
    error NftHasActiveActivities();
    error ActivityHasNotCompleted();
    error ActivityHasExpired();
    error ActivityNotExist();

    /// @notice User account error
    error InsufficientCredit();
    error InsufficientBalanceForVipLevel();
    error NoPrivilege();

    /// @notice Parameter error
    error InvalidParam();
    error NftCollectionNotSupported();
    error NftCollectionAlreadySupported();
    error ClaimableNftInsufficient();
    error TokenNotSupported();
}

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

import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
import "./IMulticall.sol";

interface IFlooring is IERC721Receiver, IMulticall {
    /// Admin Operations

    /// @notice Add new collection for Flooring Protocol
    function supportNewCollection(address _originalNFT, address fragmentToken) external;

    /// @notice Add new token which will be used as settlement token in Flooring Protocol
    /// @param addOrRemove `true` means add token, `false` means remove token
    function supportNewToken(address _tokenAddress, bool addOrRemove) external;

    /// @notice set proxy collection config
    /// Note. the `tokenId`s of the proxy collection and underlying collection must be correspond one by one
    /// eg. Paraspace Derivative Token BAYC(nBAYC) -> BAYC
    function setCollectionProxy(address proxyCollection, address underlyingCollection) external;

    /// @notice withdraw platform fee accumulated.
    /// Note. withdraw from `address(this)`'s account.
    function withdrawPlatformFee(address token, uint256 amount) external;

    /// @notice Deposit and lock credit token on behalf of receiver
    /// user can not withdraw these tokens until `unlockCredit` is called.
    function addAndLockCredit(address receiver, uint256 amount) external;

    /// @notice Unlock user credit token to allow withdraw
    /// used to release investors' funds as time goes
    /// Note. locked credit can be used to operate safeboxes(lock/unlock...)
    function unlockCredit(address receiver, uint256 amount) external;

    /// User Operations

    /// @notice User deposits token to the Floor Contract
    /// @param onBehalfOf deposit token into `onBehalfOf`'s account.(note. the tokens of msg.sender will be transfered)
    function addTokens(address onBehalfOf, address token, uint256 amount) external payable;

    /// @notice User removes token from Floor Contract
    /// @param receiver who will receive the funds.(note. the token of msg.sender will be transfered)
    function removeTokens(address token, uint256 amount, address receiver) external;

    /// @notice Lock specified `nftIds` into Flooring Safeboxes and receive corresponding Fragment Tokens of the `collection`
    /// @param expiryTs when the safeboxes expired, `0` means infinite lock without expiry
    /// @param vipLevel vip tier required in this lock operation
    /// @param maxCredit maximum credit can be locked in this operation, if real cost exceeds this limit, the tx will fail
    /// @param onBehalfOf who will receive the safebox and fragment tokens.(note. the NFTs of the msg.sender will be transfered)
    function lockNFTs(
        address collection,
        uint256[] memory nftIds,
        uint256 expiryTs,
        uint256 vipLevel,
        uint256 maxCredit,
        address onBehalfOf
    ) external returns (uint256);

    /// @notice Extend the exist safeboxes with longer lock duration with more credit token staked
    /// @param expiryTs new expiry timestamp, should bigger than previous expiry
    function extendKeys(
        address collection,
        uint256[] memory nftIds,
        uint256 expiryTs,
        uint256 vipLevel,
        uint256 maxCredit
    ) external returns (uint256);

    /// @notice Unlock specified `nftIds` which had been locked previously
    ///         sender's wallet should have enough Fragment Tokens of the `collection` which will be burned to redeem the NFTs
    /// @param expiryTs the latest nft's expiry, we need this to clear locking records
    ///                 if the value smaller than the latest nft's expiry, the tx will fail
    ///                 if part of `nftIds` were locked infinitely, just skip these expiry
    /// @param receiver who will receive the NFTs.
    ///                 note. - The safeboxes of the msg.sender will be removed.
    ///                       - The Fragment Tokens of the msg.sender will be burned.
    function unlockNFTs(address collection, uint256 expiryTs, uint256[] memory nftIds, address receiver) external;

    /// @notice Fragment specified `nftIds` into Floor Vault and receive Fragment Tokens without any locking
    ///         after fragmented, any one has enough Fragment Tokens can redeem there `nftIds`
    /// @param onBehalfOf who will receive the fragment tokens.(note. the NFTs of the msg.sender will be transfered)
    function fragmentNFTs(address collection, uint256[] memory nftIds, address onBehalfOf) external;

    /// @notice Claim `nftIds` which had been locked and had expired
    ///         sender's wallet should have enough Fragment Tokens of the `collection` which will be burned to redeem the NFTs
    /// @param maxCredit maximum credit can be costed in this operation, if real cost exceeds this limit, the tx will fail
    /// @param receiver who will receive the NFTs.
    ///                 note. - the msg.sender will pay the redemption cost.
    ///                       - The Fragment Tokens of the msg.sender will be burned.
    function claimExpiredNfts(address collection, uint256[] memory nftIds, uint256 maxCredit, address receiver)
        external
        returns (uint256);

    /// @notice Randomly claim `claimCnt` NFTs from Floor Vault
    ///         sender's wallet should have enough Fragment Tokens of the `collection` which will be burned to redeem the NFTs
    /// @param maxCredit maximum credit can be costed in this operation, if real cost exceeds this limit, the tx will fail
    /// @param receiver who will receive the NFTs.
    ///                 note. - the msg.sender will pay the redemption cost.
    ///                       - The Fragment Tokens of the msg.sender will be burned.
    function claimRandomNFT(address collection, uint256 claimCnt, uint256 maxCredit, address receiver)
        external
        returns (uint256);

    /// @notice Start auctions on specified `nftIds` with an initial bid price(`bidAmount`)
    ///         This kind of auctions will be settled with Floor Credit Token
    /// @param bidAmount initial bid price
    function initAuctionOnExpiredSafeBoxes(address collection, uint256[] memory nftIds, uint256 bidAmount) external;

    /// @notice Owner starts auctions on his locked Safeboxes
    /// @param maxExpiry the latest nft's expiry, we need this to clear locking records
    /// @param token which token should be used to settle auctions(bid, settle)
    /// @param minimumBid minimum bid price when someone place a bid on the auction
    function ownerInitAuctions(
        address collection,
        uint256[] memory nftIds,
        uint256 maxExpiry,
        address token,
        uint256 minimumBid
    ) external;

    /// @notice Place a bid on specified `nftId`'s action
    /// @param bidAmount bid price
    /// @param bidOptionIdx which option used to extend auction expiry and bid price
    function placeBidOnAuction(address collection, uint256 nftId, uint256 bidAmount, uint256 bidOptionIdx) external;

    /// @notice Place a bid on specified `nftId`'s action
    /// @param token which token should be transfered to the Flooring for bidding. `0x0` means ETH(native)
    /// @param amountToTransfer how many `token` should to transfered
    function placeBidOnAuction(
        address collection,
        uint256 nftId,
        uint256 bidAmount,
        uint256 bidOptionIdx,
        address token,
        uint256 amountToTransfer
    ) external payable;

    /// @notice Settle auctions of `nftIds`
    function settleAuctions(address collection, uint256[] memory nftIds) external;

    struct RaffleInitParam {
        address collection;
        uint256[] nftIds;
        /// @notice which token used to buy and settle raffle
        address ticketToken;
        /// @notice price per ticket
        uint96 ticketPrice;
        /// @notice max tickets amount can be sold
        uint32 maxTickets;
        /// @notice durationIdx used to get how long does raffles last
        uint256 duration;
        /// @notice the largest epxiry of nfts, we need this to clear locking records
        uint256 maxExpiry;
    }

    /// @notice Owner start raffles on locked `nftIds`
    function ownerInitRaffles(RaffleInitParam memory param) external;

    /// @notice Buy `nftId`'s raffle tickets
    /// @param ticketCnt how many tickets should be bought in this operation
    function buyRaffleTickets(address collectionId, uint256 nftId, uint256 ticketCnt) external;

    /// @notice Buy `nftId`'s raffle tickets
    /// @param token which token should be transfered to the Flooring for buying. `0x0` means ETH(native)
    /// @param amountToTransfer how many `token` should to transfered
    function buyRaffleTickets(
        address collectionId,
        uint256 nftId,
        uint256 ticketCnt,
        address token,
        uint256 amountToTransfer
    ) external payable;

    /// @notice Settle raffles of `nftIds`
    function settleRaffles(address collectionId, uint256[] memory nftIds) external;

    struct PrivateOfferInitParam {
        address collection;
        uint256[] nftIds;
        /// @notice the largest epxiry of nfts, we need this to clear locking records
        uint256 maxExpiry;
        /// @notice who will receive the otc offers
        address receiver;
        /// @notice which token used to settle offers
        address token;
        /// @notice price of the offers
        uint96 price;
    }

    /// @notice Owner start private offers(otc) on locked `nftIds`
    function ownerInitPrivateOffers(PrivateOfferInitParam memory param) external;

    /// @notice Owner or Receiver cancel the private offers of `nftIds`
    function cancelPrivateOffers(address collectionId, uint256[] memory nftIds) external;

    /// @notice Receiver accept the private offers of `nftIds`
    function buyerAcceptPrivateOffers(address collectionId, uint256[] memory nftIds) external;

    /// @notice Receiver accept the private offers of `nftIds`
    /// @param token which token should be transfered to the Flooring for buying. `0x0` means ETH(native)
    /// @param amountToTransfer how many `token` should to transfered
    function buyerAcceptPrivateOffers(
        address collectionId,
        uint256[] memory nftIds,
        address token,
        uint256 amountToTransfer
    ) external payable;

    /// @notice Clear expired or mismatching safeboxes of `nftIds` in user account
    /// @param onBehalfOf whose account will be recalculated
    /// @return credit amount has been released
    function removeExpiredKeyAndRestoreCredit(address collection, uint256[] memory nftIds, address onBehalfOf)
        external
        returns (uint256);

    /// @notice Update user's staking credit status by iterating all active collections in user account
    /// @param onBehalfOf whose account will be recalculated
    /// @return availableCredit how many credit available to use after this opeartion
    function recalculateAvailableCredit(address onBehalfOf) external returns (uint256 availableCredit);

    /// Util operations

    /// @notice Called by external contracts to access granular pool state
    /// @param slot Key of slot to sload
    /// @return value The value of the slot as bytes32
    function extsload(bytes32 slot) external view returns (bytes32 value);

    /// @notice Called by external contracts to access granular pool state
    /// @param slot Key of slot to start sloading from
    /// @param nSlots Number of slots to load into return value
    /// @return value The value of the sload-ed slots concatenated as dynamic bytes
    function extsload(bytes32 slot, uint256 nSlots) external view returns (bytes memory value);

    function creditToken() external view returns (address);
}

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

interface IFragmentToken {
    error CallerIsNotTrustedContract();

    function mint(address account, uint256 amount) external;

    function burn(address account, uint256 amount) external;
}

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

/// @title Multicall interface
/// @notice Enables calling multiple methods in a single call to the contract
interface IMulticall {
    /**
     * @dev A call to an address target failed. The target may have reverted.
     */
    error FailedMulticall();

    struct CallData {
        address target;
        bytes callData;
    }

    /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
    /// @param data The encoded function data for each of the calls to make to this contract
    /// @return results The results from each of the calls passed in via data
    function multicall(bytes[] calldata data) external returns (bytes[] memory results);

    /// @notice Allow trusted caller to call specified addresses through the Contract
    /// @dev The `msg.value` should not be trusted for any method callable from multicall.
    /// @param calls The encoded function data and target for each of the calls to make to this contract
    /// @return results The results from each of the calls passed in via calls
    function extMulticall(CallData[] calldata calls) external returns (bytes[] memory);
}

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

library ERC721Transfer {
    /// @notice Thrown when an ERC721 transfer fails
    error ERC721TransferFailed();

    function safeTransferFrom(address collection, address from, address to, uint256 tokenId) internal {
        bool success;

        /// @solidity memory-safe-assembly
        assembly {
            // Get a pointer to some free memory.
            let memPointer := mload(0x40)

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(0, 0x42842e0e00000000000000000000000000000000000000000000000000000000)
            mstore(4, from) // Append and mask the "from" argument.
            mstore(36, to) // Append and mask the "to" argument.
            // Append the "tokenId" argument. Masking not required as it's a full 32 byte type.
            mstore(68, tokenId)

            success :=
                and(
                    // Set success to whether the call reverted, if not we check it either
                    // returned exactly 1 (can't just be non-zero data), or had no return data.
                    or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                    // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
                    // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
                    // Counterintuitively, this call must be positioned second to the or() call in the
                    // surrounding and() call or else returndatasize() will be zero during the computation.
                    call(gas(), collection, 0, 0, 100, 0, 32)
                )

            mstore(0x60, 0) // Restore the zero slot to zero.
            mstore(0x40, memPointer) // Restore the memPointer.
        }

        if (!success) revert ERC721TransferFailed();
    }

    function safeBatchTransferFrom(address collection, address from, address to, uint256[] memory tokenIds) internal {
        unchecked {
            uint256 len = tokenIds.length;
            for (uint256 i; i < len; ++i) {
                safeTransferFrom(collection, from, to, tokenIds[i]);
            }
        }
    }
}

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

import "../Constants.sol";

library RollingBuckets {
    error BucketValueExceedsLimit();
    error BucketLengthExceedsLimit();

    /// @dev `MAX_BUCKET_SIZE` must be a multiple of `WORD_ELEMENT_SIZE`,
    /// otherwise some words may be incomplete which may lead to incorrect bit positioning.
    uint256 constant MAX_BUCKET_SIZE = Constants.MAX_LOCKING_BUCKET;
    /// @dev each `ELEMENT_BIT_SIZE` bits stores an element
    uint256 constant ELEMENT_BIT_SIZE = 24;
    /// @dev `ELEMENT_BIT_SIZE` bits mask
    uint256 constant MASK = 0xFFFFFF;
    /// @dev one word(256 bits) can store (256 // ELEMENT_BIT_SIZE) elements
    uint256 constant WORD_ELEMENT_SIZE = 10;

    function position(uint256 tick) private pure returns (uint256 wordPos, uint256 bitPos) {
        unchecked {
            wordPos = tick / WORD_ELEMENT_SIZE;
            bitPos = tick % WORD_ELEMENT_SIZE;
        }
    }

    function get(mapping(uint256 => uint256) storage buckets, uint256 bucketStamp) internal view returns (uint256) {
        unchecked {
            (uint256 wordPos, uint256 bitPos) = position(bucketStamp % MAX_BUCKET_SIZE);
            return (buckets[wordPos] >> (bitPos * ELEMENT_BIT_SIZE)) & MASK;
        }
    }

    /// [first, last)
    function batchGet(mapping(uint256 => uint256) storage buckets, uint256 firstStamp, uint256 lastStamp)
        internal
        view
        returns (uint256[] memory)
    {
        if (firstStamp > lastStamp) revert BucketLengthExceedsLimit();

        uint256 len;
        unchecked {
            len = lastStamp - firstStamp;
        }

        if (len > MAX_BUCKET_SIZE) {
            revert BucketLengthExceedsLimit();
        }

        uint256[] memory result = new uint256[](len);
        uint256 resultIndex;

        unchecked {
            (uint256 wordPos, uint256 bitPos) = position(firstStamp % MAX_BUCKET_SIZE);

            uint256 wordVal = buckets[wordPos];
            uint256 mask = MASK << (bitPos * ELEMENT_BIT_SIZE);

            for (uint256 i = firstStamp; i < lastStamp;) {
                assembly {
                    /// increase idx firstly to skip `array length`
                    resultIndex := add(resultIndex, 0x20)
                    /// wordVal store order starts from lowest bit
                    /// result[i] = ((wordVal & mask) >> (bitPos * ELEMENT_BIT_SIZE))
                    mstore(add(result, resultIndex), shr(mul(bitPos, ELEMENT_BIT_SIZE), and(wordVal, mask)))
                    mask := shl(ELEMENT_BIT_SIZE, mask)
                    bitPos := add(bitPos, 1)
                    i := add(i, 1)
                }

                if (bitPos == WORD_ELEMENT_SIZE) {
                    (wordPos, bitPos) = position(i % MAX_BUCKET_SIZE);

                    wordVal = buckets[wordPos];
                    mask = MASK;
                }
            }
        }

        return result;
    }

    function set(mapping(uint256 => uint256) storage buckets, uint256 bucketStamp, uint256 value) internal {
        if (value > MASK) revert BucketValueExceedsLimit();

        unchecked {
            (uint256 wordPos, uint256 bitPos) = position(bucketStamp % MAX_BUCKET_SIZE);

            uint256 wordValue = buckets[wordPos];
            uint256 newValue = value << (bitPos * ELEMENT_BIT_SIZE);

            uint256 newWord = (wordValue & ~(MASK << (bitPos * ELEMENT_BIT_SIZE))) | newValue;
            buckets[wordPos] = newWord;
        }
    }

    function batchSet(mapping(uint256 => uint256) storage buckets, uint256 firstStamp, uint256[] memory values)
        internal
    {
        uint256 valLength = values.length;
        if (valLength > MAX_BUCKET_SIZE) revert BucketLengthExceedsLimit();
        if (firstStamp > (type(uint256).max - valLength)) {
            revert BucketLengthExceedsLimit();
        }

        unchecked {
            (uint256 wordPos, uint256 bitPos) = position(firstStamp % MAX_BUCKET_SIZE);

            uint256 wordValue = buckets[wordPos];
            uint256 mask = ~(MASK << (bitPos * ELEMENT_BIT_SIZE));

            /// reuse val length as End Postion
            valLength = (valLength + 1) * 0x20;
            /// start from first element offset
            for (uint256 i = 0x20; i < valLength; i += 0x20) {
                uint256 val;
                assembly {
                    val := mload(add(values, i))
                }
                if (val > MASK) revert BucketValueExceedsLimit();

                assembly {
                    /// newVal = val << (bitPos * BIT_SIZE)
                    let newVal := shl(mul(bitPos, ELEMENT_BIT_SIZE), val)
                    /// save newVal to wordVal, clear corresponding bits and set them as newVal
                    /// wordValue = (wordVal & mask) | newVal
                    wordValue := or(and(wordValue, mask), newVal)
                    /// goto next number idx in current word
                    bitPos := add(bitPos, 1)
                    /// mask = ~(MASK << (bitPos, BIT_SIZE))
                    mask := not(shl(mul(bitPos, ELEMENT_BIT_SIZE), MASK))
                }

                if (bitPos == WORD_ELEMENT_SIZE) {
                    /// store hole word
                    buckets[wordPos] = wordValue;

                    /// get next word' position
                    (wordPos, bitPos) = position((firstStamp + (i / 0x20)) % MAX_BUCKET_SIZE);
                    wordValue = buckets[wordPos];
                    /// restore mask to make it start from lowest bits
                    mask = ~MASK;
                }
            }
            /// store last word which may incomplete
            buckets[wordPos] = wordValue;
        }
    }
}

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

import "openzeppelin-contracts/contracts/utils/math/Math.sol";
import "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";

import "../library/RollingBuckets.sol";
import "../library/ERC721Transfer.sol";

import "../Errors.sol";
import "../Constants.sol";
import "./User.sol";
import "./Helper.sol";
import {SafeBox, CollectionState, AuctionInfo, CollectionAccount, UserFloorAccount, LockParam} from "./Structs.sol";
import {SafeBoxLib} from "./SafeBox.sol";

import "../interface/IFlooring.sol";

library CollectionLib {
    using SafeBoxLib for SafeBox;
    using SafeCast for uint256;
    using RollingBuckets for mapping(uint256 => uint256);
    using UserLib for CollectionAccount;
    using UserLib for UserFloorAccount;

    event LockNft(
        address indexed sender,
        address indexed onBehalfOf,
        address indexed collection,
        uint256[] tokenIds,
        uint256[] safeBoxKeys,
        uint256 safeBoxExpiryTs,
        uint256 minMaintCredit,
        address proxyCollection
    );
    event ExtendKey(
        address indexed operator,
        address indexed collection,
        uint256[] tokenIds,
        uint256[] safeBoxKeys,
        uint256 safeBoxExpiryTs,
        uint256 minMaintCredit
    );
    event UnlockNft(
        address indexed operator,
        address indexed receiver,
        address indexed collection,
        uint256[] tokenIds,
        address proxyCollection
    );
    event RemoveExpiredKey(
        address indexed operator,
        address indexed onBehalfOf,
        address indexed collection,
        uint256[] tokenIds,
        uint256[] safeBoxKeys
    );
    event ClaimExpiredNft(
        address indexed operator,
        address indexed receiver,
        address indexed collection,
        uint256[] tokenIds,
        uint256 creditCost,
        address proxyCollection
    );
    event FragmentNft(
        address indexed operator, address indexed onBehalfOf, address indexed collection, uint256[] tokenIds
    );
    event ClaimRandomNft(
        address indexed operator,
        address indexed receiver,
        address indexed collection,
        uint256[] tokenIds,
        uint256 creditCost
    );

    function fragmentNFTs(
        CollectionState storage collectionState,
        address collection,
        uint256[] memory nftIds,
        address onBehalfOf
    ) public {
        uint256 nftLen = nftIds.length;
        unchecked {
            for (uint256 i; i < nftLen; ++i) {
                collectionState.freeTokenIds.push(nftIds[i]);
            }
        }
        collectionState.floorToken.mint(onBehalfOf, Constants.FLOOR_TOKEN_AMOUNT * nftLen);
        ERC721Transfer.safeBatchTransferFrom(collection, msg.sender, address(this), nftIds);

        emit FragmentNft(msg.sender, onBehalfOf, collection, nftIds);
    }

    struct LockInfo {
        bool isInfinite;
        uint256 currentBucket;
        uint256 newExpiryBucket;
        uint256 totalManaged;
        uint256 newRequireLockCredit;
        uint64 infiniteCnt;
    }

    function lockNfts(
        CollectionState storage collection,
        UserFloorAccount storage account,
        LockParam memory param,
        address onBehalfOf
    ) public returns (uint256 totalCreditCost) {
        if (onBehalfOf == address(this)) revert Errors.InvalidParam();

        uint8 vipLevel = uint8(param.vipLevel);
        uint256 totalCredit = account.ensureVipCredit(vipLevel, param.creditToken);
        Helper.ensureMaxLocking(collection, vipLevel, param.expiryTs, param.nftIds.length);
        Helper.ensureProxyVipLevel(Constants.getVipLevel(totalCredit), param.collection != param.proxyCollection);

        /// cache value to avoid multi-reads
        uint256 minMaintCredit = account.minMaintCredit;
        uint256[] memory nftIds = param.nftIds;
        uint256[] memory newKeys;
        {
            CollectionAccount storage userCollectionAccount = account.getOrAddCollection(param.collection);

            (totalCreditCost, newKeys) = _lockNfts(collection, userCollectionAccount, nftIds, param.expiryTs, vipLevel);

            // compute max credit for locking cost
            uint96 totalLockingCredit = userCollectionAccount.totalLockingCredit;
            {
                uint256 creditBuffer;
                unchecked {
                    creditBuffer = totalCredit - totalLockingCredit;
                }
                if (totalCreditCost > creditBuffer || totalCreditCost > param.maxCreditCost) {
                    revert Errors.InsufficientCredit();
                }
            }

            totalLockingCredit += totalCreditCost.toUint96();
            userCollectionAccount.totalLockingCredit = totalLockingCredit;

            if (totalLockingCredit > minMaintCredit) {
                account.minMaintCredit = totalLockingCredit;
                minMaintCredit = totalLockingCredit;
            }
        }

        account.updateVipKeyCount(vipLevel, int256(nftIds.length));
        /// mint for `onBehalfOf`, transfer from msg.sender
        collection.floorToken.mint(onBehalfOf, Constants.FLOOR_TOKEN_AMOUNT * nftIds.length);
        ERC721Transfer.safeBatchTransferFrom(param.proxyCollection, msg.sender, address(this), nftIds);

        emit LockNft(
            msg.sender,
            onBehalfOf,
            param.collection,
            nftIds,
            newKeys,
            param.expiryTs,
            minMaintCredit,
            param.proxyCollection
        );
    }

    function _lockNfts(
        CollectionState storage collectionState,
        CollectionAccount storage account,
        uint256[] memory nftIds,
        uint256 expiryTs, // treat 0 as infinite lock.
        uint8 vipLevel
    ) private returns (uint256, uint256[] memory) {
        LockInfo memory info = LockInfo({
            isInfinite: expiryTs == 0,
            currentBucket: Helper.counterStamp(block.timestamp),
            newExpiryBucket: Helper.counterStamp(expiryTs),
            totalManaged: collectionState.activeSafeBoxCnt + collectionState.freeTokenIds.length,
            newRequireLockCredit: 0,
            infiniteCnt: collectionState.infiniteCnt
        });
        if (info.isInfinite) {
            /// if it is infinite lock, we need load all buckets to calculate the staking cost
            info.newExpiryBucket = Helper.counterStamp(block.timestamp + Constants.MAX_LOCKING_PERIOD);
        }

        uint256[] memory buckets = Helper.prepareBucketUpdate(collectionState, info.currentBucket, info.newExpiryBucket);
        /// @dev `keys` used to log info, we just compact its fields into one 256 bits number
        uint256[] memory keys = new uint256[](nftIds.length);

        for (uint256 idx; idx < nftIds.length;) {
            uint256 lockedCredit = updateCountersAndGetSafeboxCredit(buckets, info, vipLevel);

            if (info.isInfinite) ++info.infiniteCnt;

            SafeBoxKey memory key = SafeBoxKey({
                keyId: Helper.generateNextKeyId(collectionState),
                lockingCredit: lockedCredit.toUint96(),
                vipLevel: vipLevel
            });

            account.addSafeboxKey(nftIds[idx], key);
            addSafeBox(
                collectionState, nftIds[idx], SafeBox({keyId: key.keyId, expiryTs: uint32(expiryTs), owner: msg.sender})
            );

            keys[idx] = SafeBoxLib.encodeSafeBoxKey(key);

            info.newRequireLockCredit += lockedCredit;
            unchecked {
                ++info.totalManaged;
                ++idx;
            }
        }

        if (info.isInfinite) {
            collectionState.infiniteCnt = info.infiniteCnt;
        } else {
            collectionState.countingBuckets.batchSet(info.currentBucket, buckets);
            if (info.newExpiryBucket > collectionState.lastUpdatedBucket) {
                collectionState.lastUpdatedBucket = uint64(info.newExpiryBucket);
            }
        }

        return (info.newRequireLockCredit, keys);
    }

    function unlockNfts(
        CollectionState storage collection,
        UserFloorAccount storage userAccount,
        address proxyCollection,
        address collectionId,
        uint256[] memory nftIds,
        uint256 maxExpiryTs,
        address receiver
    ) public {
        CollectionAccount storage userCollectionAccount = userAccount.getByKey(collectionId);
        SafeBoxKey[] memory releasedKeys = _unlockNfts(collection, maxExpiryTs, nftIds, userCollectionAccount);

        collection.floorToken.burn(msg.sender, Constants.FLOOR_TOKEN_AMOUNT * nftIds.length);

        for (uint256 i = 0; i < releasedKeys.length;) {
            userAccount.updateVipKeyCount(releasedKeys[i].vipLevel, -1);
            unchecked {
                ++i;
            }
        }
        ERC721Transfer.safeBatchTransferFrom(proxyCollection, address(this), receiver, nftIds);

        emit UnlockNft(msg.sender, receiver, collectionId, nftIds, proxyCollection);
    }

    function _unlockNfts(
        CollectionState storage collectionState,
        uint256 maxExpiryTs,
        uint256[] memory nftIds,
        CollectionAccount storage userCollectionAccount
    ) private returns (SafeBoxKey[] memory) {
        if (maxExpiryTs > 0 && maxExpiryTs < block.timestamp) revert Errors.SafeBoxHasExpire();
        SafeBoxKey[] memory expiredKeys = new SafeBoxKey[](nftIds.length);
        uint256 currentBucketTime = Helper.counterStamp(block.timestamp);
        uint256 creditToRelease = 0;
        uint256[] memory buckets;

        /// if maxExpiryTs == 0, it means all nftIds in this batch being locked infinitely that we don't need to update countingBuckets
        if (maxExpiryTs > 0) {
            uint256 maxExpiryBucketTime = Math.min(Helper.counterStamp(maxExpiryTs), collectionState.lastUpdatedBucket);
            buckets = collectionState.countingBuckets.batchGet(currentBucketTime, maxExpiryBucketTime);
        }

        for (uint256 i; i < nftIds.length;) {
            uint256 nftId = nftIds[i];

            if (Helper.hasActiveActivities(collectionState, nftId)) revert Errors.NftHasActiveActivities();

            (SafeBox storage safeBox, SafeBoxKey storage safeBoxKey) =
                Helper.useSafeBoxAndKey(collectionState, userCollectionAccount, nftId);

            creditToRelease += safeBoxKey.lockingCredit;
            if (safeBox.isInfiniteSafeBox()) {
                --collectionState.infiniteCnt;
            } else {
                uint256 limit = Helper.counterStamp(safeBox.expiryTs) - currentBucketTime;
                if (limit > buckets.length) revert();
                for (uint256 idx; idx < limit;) {
                    --buckets[idx];
                    unchecked {
                        ++idx;
                    }
                }
            }

            expiredKeys[i] = safeBoxKey;

            removeSafeBox(collectionState, nftId);
            userCollectionAccount.removeSafeboxKey(nftId);

            unchecked {
                ++i;
            }
        }

        userCollectionAccount.totalLockingCredit -= creditToRelease.toUint96();
        if (buckets.length > 0) {
            collectionState.countingBuckets.batchSet(currentBucketTime, buckets);
        }

        return expiredKeys;
    }

    function claimExpiredNfts(
        CollectionState storage collectionState,
        mapping(address => UserFloorAccount) storage userAccounts,
        address creditToken,
        address proxyCollection,
        address collectionId,
        uint256[] memory nftIds,
        uint256 maxCreditCost,
        address receiver
    ) public returns (uint256 totalCreditCost) {
        for (uint256 i = 0; i < nftIds.length;) {
            SafeBox storage safeBox = Helper.useSafeBox(collectionState, nftIds[i]);
            if (!safeBox.isSafeBoxExpired()) revert Errors.SafeBoxHasNotExpire();
            if (!Helper.isAuctionPeriodOver(safeBox)) revert Errors.AuctionHasNotCompleted();

            removeSafeBox(collectionState, nftIds[i]);

            unchecked {
                ++i;
            }
        }

        uint256 currentlyLocked =
            Helper.getActiveSafeBoxes(collectionState, block.timestamp) + collectionState.infiniteCnt;
        UserFloorAccount storage userAccount = userAccounts[msg.sender];

        totalCreditCost = nftIds.length
            * Constants.getClaimExpiredCost(
                currentlyLocked,
                collectionState.activeSafeBoxCnt + collectionState.freeTokenIds.length,
                Constants.getVipLevel(userAccount.tokenBalance(creditToken))
            );
        if (totalCreditCost > maxCreditCost) {
            revert Errors.InsufficientCredit();
        }

        userAccount.transferToken(userAccounts[address(this)], creditToken, totalCreditCost, true);
        collectionState.floorToken.burn(msg.sender, Constants.FLOOR_TOKEN_AMOUNT * nftIds.length);
        ERC721Transfer.safeBatchTransferFrom(proxyCollection, address(this), receiver, nftIds);

        emit ClaimExpiredNft(msg.sender, receiver, collectionId, nftIds, totalCreditCost, proxyCollection);
    }

    function extendLockingForKeys(
        CollectionState storage collection,
        UserFloorAccount storage userAccount,
        LockParam memory param
    ) public returns (uint256 totalCreditCost) {
        uint8 newVipLevel = uint8(param.vipLevel);
        uint256 totalCredit = userAccount.ensureVipCredit(newVipLevel, param.creditToken);
        Helper.ensureMaxLocking(collection, newVipLevel, param.expiryTs, param.nftIds.length);

        uint256 minMaintCredit = userAccount.minMaintCredit;
        uint256[] memory safeBoxKeys;
        {
            CollectionAccount storage collectionAccount = userAccount.getOrAddCollection(param.collection);

            // extend lock duration
            int256[] memory vipLevelDiffs;
            (vipLevelDiffs, totalCreditCost, safeBoxKeys) =
                _extendLockingForKeys(collection, collectionAccount, param.nftIds, param.expiryTs, uint8(newVipLevel));

            // compute max credit for locking cost
            uint96 totalLockingCredit = collectionAccount.totalLockingCredit;
            {
                uint256 creditBuffer;
                unchecked {
                    creditBuffer = totalCredit - totalLockingCredit;
                }
                if (totalCreditCost > creditBuffer || totalCreditCost > param.maxCreditCost) {
                    revert Errors.InsufficientCredit();
                }
            }

            // update user vip key counts
            for (uint256 vipLevel = 0; vipLevel < vipLevelDiffs.length;) {
                userAccount.updateVipKeyCount(uint8(vipLevel), vipLevelDiffs[vipLevel]);
                unchecked {
                    ++vipLevel;
                }
            }

            totalLockingCredit += totalCreditCost.toUint96();
            collectionAccount.totalLockingCredit = totalLockingCredit;
            if (totalLockingCredit > minMaintCredit) {
                userAccount.minMaintCredit = totalLockingCredit;
                minMaintCredit = totalLockingCredit;
            }
        }

        emit ExtendKey(msg.sender, param.collection, param.nftIds, safeBoxKeys, param.expiryTs, minMaintCredit);
    }

    function _extendLockingForKeys(
        CollectionState storage collectionState,
        CollectionAccount storage userCollectionAccount,
        uint256[] memory nftIds,
        uint256 newExpiryTs, // expiryTs of 0 is infinite.
        uint8 newVipLevel
    ) private returns (int256[] memory, uint256, uint256[] memory) {
        LockInfo memory info = LockInfo({
            isInfinite: newExpiryTs == 0,
            currentBucket: Helper.counterStamp(block.timestamp),
            newExpiryBucket: Helper.counterStamp(newExpiryTs),
            totalManaged: collectionState.activeSafeBoxCnt + collectionState.freeTokenIds.length,
            newRequireLockCredit: 0,
            infiniteCnt: collectionState.infiniteCnt
        });
        if (info.isInfinite) {
            info.newExpiryBucket = Helper.counterStamp(block.timestamp + Constants.MAX_LOCKING_PERIOD);
        }

        uint256[] memory buckets = Helper.prepareBucketUpdate(collectionState, info.currentBucket, info.newExpiryBucket);
        int256[] memory vipLevelDiffs = new int256[](Constants.VIP_LEVEL_COUNT);
        /// @dev `keys` used to log info, we just compact its fields into one 256 bits number
        uint256[] memory keys = new uint256[](nftIds.length);

        for (uint256 idx; idx < nftIds.length;) {
            if (Helper.hasActiveActivities(collectionState, nftIds[idx])) revert Errors.NftHasActiveActivities();

            (SafeBox storage safeBox, SafeBoxKey storage safeBoxKey) =
                Helper.useSafeBoxAndKey(collectionState, userCollectionAccount, nftIds[idx]);

            {
                uint256 extendOffset = Helper.counterStamp(safeBox.expiryTs) - info.currentBucket;
                unchecked {
                    for (uint256 i; i < extendOffset; ++i) {
                        if (buckets[i] == 0) revert Errors.InvalidParam();
                        --buckets[i];
                    }
                }
            }

            uint256 safeboxQuote = updateCountersAndGetSafeboxCredit(buckets, info, newVipLevel);

            if (safeboxQuote > safeBoxKey.lockingCredit) {
                info.newRequireLockCredit += (safeboxQuote - safeBoxKey.lockingCredit);
                safeBoxKey.lockingCredit = safeboxQuote.toUint96();
            }

            uint8 oldVipLevel = safeBoxKey.vipLevel;
            if (newVipLevel > oldVipLevel) {
                safeBoxKey.vipLevel = newVipLevel;
                --vipLevelDiffs[oldVipLevel];
                ++vipLevelDiffs[newVipLevel];
            }

            if (info.isInfinite) {
                safeBox.expiryTs = 0;
                ++info.infiniteCnt;
            } else {
                safeBox.expiryTs = uint32(newExpiryTs);
            }

            keys[idx] = SafeBoxLib.encodeSafeBoxKey(safeBoxKey);

            unchecked {
                ++idx;
            }
        }

        if (info.isInfinite) {
            collectionState.infiniteCnt = info.infiniteCnt;
        } else {
            collectionState.countingBuckets.batchSet(info.currentBucket, buckets);
            if (info.newExpiryBucket > collectionState.lastUpdatedBucket) {
                collectionState.lastUpdatedBucket = uint64(info.newExpiryBucket);
            }
        }
        return (vipLevelDiffs, info.newRequireLockCredit, keys);
    }

    function updateCountersAndGetSafeboxCredit(uint256[] memory counters, LockInfo memory lockInfo, uint8 vipLevel)
        private
        pure
        returns (uint256 result)
    {
        unchecked {
            uint256 infiniteCnt = lockInfo.infiniteCnt;
            uint256 totalManaged = lockInfo.totalManaged;

            uint256 counterOffsetEnd = (counters.length + 1) * 0x20;
            uint256 tmpCount;
            if (lockInfo.isInfinite) {
                for (uint256 i = 0x20; i < counterOffsetEnd; i += 0x20) {
                    assembly {
                        tmpCount := mload(add(counters, i))
                    }
                    result += Constants.getRequiredStakingForLockRatio(infiniteCnt + tmpCount, totalManaged);
                }
            } else {
                for (uint256 i = 0x20; i < counterOffsetEnd; i += 0x20) {
                    assembly {
                        tmpCount := mload(add(counters, i))
                    }
                    result += Constants.getRequiredStakingForLockRatio(infiniteCnt + tmpCount, totalManaged);
                    assembly {
                        /// increase counters[i]
                        mstore(add(counters, i), add(tmpCount, 1))
                    }
                }
            }
            result = Constants.getVipRequiredStakingWithDiscount(result, vipLevel);
        }
    }

    function removeExpiredKeysAndRestoreCredits(
        CollectionState storage collectionState,
        UserFloorAccount storage userAccount,
        address collectionId,
        uint256[] memory nftIds,
        address onBehalfOf
    ) public returns (uint256 releasedCredit) {
        CollectionAccount storage collectionAccount = userAccount.getByKey(collectionId);

        uint256 removedCnt;
        uint256[] memory removedIds = new uint256[](nftIds.length);
        uint256[] memory removedKeys = new uint256[](nftIds.length);
        for (uint256 i = 0; i < nftIds.length;) {
            uint256 nftId = nftIds[i];
            SafeBoxKey memory safeBoxKey = collectionAccount.getByKey(nftId);
            SafeBox memory safeBox = collectionState.safeBoxes[nftId];

            if (safeBoxKey.keyId == 0) {
                revert Errors.InvalidParam();
            }

            if (safeBox._isSafeBoxExpired() || !safeBox._isKeyMatchingSafeBox(safeBoxKey)) {
                removedIds[removedCnt] = nftId;
                removedKeys[removedCnt] = SafeBoxLib.encodeSafeBoxKey(safeBoxKey);

                unchecked {
                    ++removedCnt;
                    releasedCredit += safeBoxKey.lockingCredit;
                }

                userAccount.updateVipKeyCount(safeBoxKey.vipLevel, -1);
                collectionAccount.removeSafeboxKey(nftId);
            }

            unchecked {
                ++i;
            }
        }

        if (releasedCredit > 0) {
            collectionAccount.totalLockingCredit -= releasedCredit.toUint96();
        }

        emit RemoveExpiredKey(msg.sender, onBehalfOf, collectionId, removedIds, removedKeys);
    }

    function claimRandomNFT(
        CollectionState storage collection,
        mapping(address => UserFloorAccount) storage userAccounts,
        address creditToken,
        address collectionId,
        uint256 claimCnt,
        uint256 maxCreditCost,
        address receiver
    ) public returns (uint256 totalCreditCost) {
        if (claimCnt == 0 || collection.freeTokenIds.length < claimCnt) revert Errors.ClaimableNftInsufficient();

        uint256 currentlyLocked = Helper.getActiveSafeBoxes(collection, block.timestamp) + collection.infiniteCnt;
        uint256 totalManaged = collection.activeSafeBoxCnt + collection.freeTokenIds.length;

        uint256[] memory selectedTokenIds = new uint256[](claimCnt);

        UserFloorAccount storage userAccount = userAccounts[msg.sender];
        uint8 vipLevel = Constants.getVipLevel(userAccount.tokenBalance(creditToken));
        while (claimCnt > 0) {
            totalCreditCost += Constants.getClaimRandomCost(currentlyLocked, totalManaged, vipLevel);

            /// just compute a deterministic random number
            uint256 chosenNftIdx = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, totalManaged)))
                % collection.freeTokenIds.length;

            unchecked {
                --claimCnt;
                --totalManaged;
            }

            selectedTokenIds[claimCnt] = collection.freeTokenIds[chosenNftIdx];

            collection.freeTokenIds[chosenNftIdx] = collection.freeTokenIds[collection.freeTokenIds.length - 1];
            collection.freeTokenIds.pop();
        }

        if (totalCreditCost > maxCreditCost) {
            revert Errors.InsufficientCredit();
        }

        userAccount.transferToken(userAccounts[address(this)], creditToken, totalCreditCost, true);
        collection.floorToken.burn(msg.sender, Constants.FLOOR_TOKEN_AMOUNT * selectedTokenIds.length);
        ERC721Transfer.safeBatchTransferFrom(collectionId, address(this), receiver, selectedTokenIds);

        emit ClaimRandomNft(msg.sender, receiver, collectionId, selectedTokenIds, totalCreditCost);
    }

    function getLockingBuckets(CollectionState storage collection, uint256 startTimestamp, uint256 endTimestamp)
        public
        view
        returns (uint256[] memory)
    {
        return Helper.prepareBucketUpdate(
            collection,
            Helper.counterStamp(startTimestamp),
            Math.min(collection.lastUpdatedBucket, Helper.counterStamp(endTimestamp))
        );
    }

    function addSafeBox(CollectionState storage collectionState, uint256 nftId, SafeBox memory safebox) internal {
        if (collectionState.safeBoxes[nftId].keyId > 0) revert Errors.SafeBoxAlreadyExist();
        collectionState.safeBoxes[nftId] = safebox;
        ++collectionState.activeSafeBoxCnt;
    }

    function removeSafeBox(CollectionState storage collectionState, uint256 nftId) internal {
        delete collectionState.safeBoxes[nftId];
        --collectionState.activeSafeBoxCnt;
    }
}

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

import "../Constants.sol";
import "../Errors.sol";
import "./SafeBox.sol";
import "./User.sol";
import {SafeBox, CollectionState, AuctionInfo, CollectionAccount, SafeBoxKey} from "./Structs.sol";
import "../library/RollingBuckets.sol";

library Helper {
    using SafeBoxLib for SafeBox;
    using UserLib for CollectionAccount;
    using RollingBuckets for mapping(uint256 => uint256);

    function counterStamp(uint256 timestamp) internal pure returns (uint96) {
        unchecked {
            return uint96((timestamp + Constants.BUCKET_SPAN_1) / Constants.BUCKET_SPAN);
        }
    }

    function ensureProxyVipLevel(uint8 vipLevel, bool proxy) internal pure {
        if (proxy && vipLevel < Constants.PROXY_COLLECTION_VIP_THRESHOLD) {
            revert Errors.InvalidParam();
        }
    }

    function ensureMaxLocking(
        CollectionState storage collection,
        uint8 vipLevel,
        uint256 requireExpiryTs,
        uint256 requireLockCnt
    ) internal view {
        /// vip level 0 can not use safebox utilities.
        if (vipLevel >= Constants.VIP_LEVEL_COUNT || vipLevel == 0) {
            revert Errors.InvalidParam();
        }

        /// only check when it is not infinite lock
        if (requireExpiryTs > 0) {
            uint256 deltaBucket;
            unchecked {
                deltaBucket = counterStamp(requireExpiryTs) - counterStamp(block.timestamp);
            }
            if (deltaBucket == 0 || deltaBucket > Constants.getVipLockingBuckets(vipLevel)) {
                revert Errors.InvalidParam();
            }
        } else {
            uint256 expectLocked =
                requireLockCnt + collection.infiniteCnt + getActiveSafeBoxes(collection, block.timestamp);
            uint256 totalManaged = requireLockCnt + collection.activeSafeBoxCnt + collection.freeTokenIds.length;
            uint256 lockingRatio = (expectLocked * 100 / totalManaged);
            uint256 restrictRatio = Constants.getLockingRatioForInfinite(vipLevel);
            if (lockingRatio > restrictRatio) {
                revert Errors.InvalidParam();
            }
        }
    }

    function useSafeBoxAndKey(CollectionState storage collection, CollectionAccount storage userAccount, uint256 nftId)
        internal
        view
        returns (SafeBox storage safeBox, SafeBoxKey storage key)
    {
        safeBox = collection.safeBoxes[nftId];
        if (safeBox.keyId == 0) revert Errors.SafeBoxNotExist();
        if (safeBox.isSafeBoxExpired()) revert Errors.SafeBoxHasExpire();

        key = userAccount.getByKey(nftId);
        if (!safeBox.isKeyMatchingSafeBox(key)) revert Errors.NoMatchingSafeBoxKey();
    }

    function useSafeBox(CollectionState storage collection, uint256 nftId)
        internal
        view
        returns (SafeBox storage safeBox)
    {
        safeBox = collection.safeBoxes[nftId];
        if (safeBox.keyId == 0) revert Errors.SafeBoxNotExist();
    }

    function generateNextKeyId(CollectionState storage collectionState) internal returns (uint64 nextKeyId) {
        nextKeyId = collectionState.nextKeyId;
        ++collectionState.nextKeyId;
    }

    function generateNextActivityId(CollectionState storage collection) internal returns (uint64 nextActivityId) {
        nextActivityId = collection.nextActivityId;
        ++collection.nextActivityId;
    }

    function isAuctionPeriodOver(SafeBox storage safeBox) internal view returns (bool) {
        return safeBox.expiryTs + Constants.FREE_AUCTION_PERIOD < block.timestamp;
    }

    function hasActiveActivities(CollectionState storage collection, uint256 nftId) internal view returns (bool) {
        return hasActiveAuction(collection, nftId) || hasActiveRaffle(collection, nftId)
            || hasActivePrivateOffer(collection, nftId);
    }

    function hasActiveAuction(CollectionState storage collection, uint256 nftId) internal view returns (bool) {
        return collection.activeAuctions[nftId].endTime >= block.timestamp;
    }

    function hasActiveRaffle(CollectionState storage collection, uint256 nftId) internal view returns (bool) {
        return collection.activeRaffles[nftId].endTime >= block.timestamp;
    }

    function hasActivePrivateOffer(CollectionState storage collection, uint256 nftId) internal view returns (bool) {
        return collection.activePrivateOffers[nftId].endTime >= block.timestamp;
    }

    function getTokenFeeRateBips(address creditToken, address floorToken, address settleToken)
        internal
        pure
        returns (uint256)
    {
        uint256 feeRateBips = Constants.COMMON_FEE_RATE_BIPS;
        if (settleToken == creditToken) {
            feeRateBips = Constants.CREDIT_FEE_RATE_BIPS;
        } else if (settleToken == floorToken) {
            feeRateBips = Constants.SPEC_FEE_RATE_BIPS;
        }

        return feeRateBips;
    }

    function calculateActivityFee(uint256 settleAmount, uint256 feeRateBips)
        internal
        pure
        returns (uint256 afterFee, uint256 fee)
    {
        fee = settleAmount * feeRateBips / 10000;
        unchecked {
            afterFee = settleAmount - fee;
        }
    }

    function prepareBucketUpdate(CollectionState storage collection, uint256 startBucket, uint256 endBucket)
        internal
        view
        returns (uint256[] memory buckets)
    {
        uint256 validEnd = collection.lastUpdatedBucket;
        uint256 padding;
        if (endBucket < validEnd) {
            validEnd = endBucket;
        } else {
            unchecked {
                padding = endBucket - validEnd;
            }
        }

        if (startBucket < validEnd) {
            if (padding == 0) {
                buckets = collection.countingBuckets.batchGet(startBucket, validEnd);
            } else {
                uint256 validLen;
                unchecked {
                    validLen = validEnd - startBucket;
                }
                buckets = new uint256[](validLen + padding);
                uint256[] memory tmp = collection.countingBuckets.batchGet(startBucket, validEnd);
                for (uint256 i; i < validLen;) {
                    buckets[i] = tmp[i];
                    unchecked {
                        ++i;
                    }
                }
            }
        } else {
            buckets = new uint256[](endBucket - startBucket);
        }
    }

    function getActiveSafeBoxes(CollectionState storage collectionState, uint256 timestamp)
        internal
        view
        returns (uint256)
    {
        uint256 bucketStamp = counterStamp(timestamp);
        if (collectionState.lastUpdatedBucket < bucketStamp) {
            return 0;
        }
        return collectionState.countingBuckets.get(bucketStamp);
    }
}

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

import {SafeBox, SafeBoxKey} from "./Structs.sol";

library SafeBoxLib {
    uint64 public constant SAFEBOX_KEY_NOTATION = type(uint64).max;

    function isInfiniteSafeBox(SafeBox storage safeBox) internal view returns (bool) {
        return safeBox.expiryTs == 0;
    }

    function isSafeBoxExpired(SafeBox storage safeBox) internal view returns (bool) {
        return safeBox.expiryTs != 0 && safeBox.expiryTs < block.timestamp;
    }

    function _isSafeBoxExpired(SafeBox memory safeBox) internal view returns (bool) {
        return safeBox.expiryTs != 0 && safeBox.expiryTs < block.timestamp;
    }

    function isKeyMatchingSafeBox(SafeBox storage safeBox, SafeBoxKey storage safeBoxKey)
        internal
        view
        returns (bool)
    {
        return safeBox.keyId == safeBoxKey.keyId;
    }

    function _isKeyMatchingSafeBox(SafeBox memory safeBox, SafeBoxKey memory safeBoxKey) internal pure returns (bool) {
        return safeBox.keyId == safeBoxKey.keyId;
    }

    function encodeSafeBoxKey(SafeBoxKey memory key) internal pure returns (uint256) {
        uint256 val = key.lockingCredit;
        val |= (uint256(key.keyId) << 96);
        val |= (uint256(key.vipLevel) << 160);
        return val;
    }
}

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

import "../interface/IFragmentToken.sol";

struct SafeBox {
    /// Either matching a key OR Constants.SAFEBOX_KEY_NOTATION meaning temporarily
    /// held by a bidder in auction.
    uint64 keyId;
    /// The timestamp that the safe box expires.
    uint32 expiryTs;
    /// The owner of the safebox. It maybe outdated due to expiry
    address owner;
}

struct PrivateOffer {
    /// private offer end time
    uint96 endTime;
    /// which token used to accpet the offer
    address token;
    /// price of the offer
    uint96 price;
    address owner;
    /// who should receive the offer
    address buyer;
    uint64 activityId;
}

struct AuctionInfo {
    /// The end time for the auction.
    uint96 endTime;
    /// Bid token address.
    address bidTokenAddress;
    /// Minimum Bid.
    uint96 minimumBid;
    /// The person who trigger the auction at the beginning.
    address triggerAddress;
    uint96 lastBidAmount;
    address lastBidder;
    /// Whether the auction is triggered by the NFT owner itself?
    bool isSelfTriggered;
    uint64 activityId;
    uint32 feeRateBips;
}

struct TicketRecord {
    /// who buy the tickets
    address buyer;
    /// Start index of tickets
    /// [startIdx, endIdx)
    uint48 startIdx;
    /// End index of tickets
    uint48 endIdx;
}

struct RaffleInfo {
    /// raffle end time
    uint48 endTime;
    /// max tickets amount the raffle can sell
    uint48 maxTickets;
    /// which token used to buy the raffle tickets
    address token;
    /// price per ticket
    uint96 ticketPrice;
    /// total funds collected by selling tickets
    uint96 collectedFund;
    uint64 activityId;
    address owner;
    /// total sold tickets amount
    uint48 ticketSold;
    uint32 feeRateBips;
    /// whether the raffle is being settling
    bool isSettling;
    /// tickets sold records
    TicketRecord[] tickets;
}

struct CollectionState {
    /// The address of the Floor Token cooresponding to the NFTs.
    IFragmentToken floorToken;
    /// Records the active safe box in each time bucket.
    mapping(uint256 => uint256) countingBuckets;
    /// Stores all of the NFTs that has been fragmented but *without* locked up limit.
    uint256[] freeTokenIds;
    /// Huge map for all the `SafeBox`es in one collection.
    mapping(uint256 => SafeBox) safeBoxes;
    /// Stores all the ongoing auctions: nftId => `AuctionInfo`.
    mapping(uint256 => AuctionInfo) activeAuctions;
    /// Stores all the ongoing raffles: nftId => `RaffleInfo`.
    mapping(uint256 => RaffleInfo) activeRaffles;
    /// Stores all the ongoing private offers: nftId => `PrivateOffer`.
    mapping(uint256 => PrivateOffer) activePrivateOffers;
    /// The last bucket time the `countingBuckets` is updated.
    uint64 lastUpdatedBucket;
    /// Next Key Id. This should start from 1, we treat key id `SafeboxLib.SAFEBOX_KEY_NOTATION` as temporarily
    /// being used for activities(auction/raffle).
    uint64 nextKeyId;
    /// Active Safe Box Count.
    uint64 activeSafeBoxCnt;
    /// The number of infinite lock count.
    uint64 infiniteCnt;
    /// Next Activity Id. This should start from 1
    uint64 nextActivityId;
}

struct UserFloorAccount {
    /// @notice it should be maximum of the `totalLockingCredit` across all collections
    uint96 minMaintCredit;
    /// @notice used to iterate collection accounts
    /// packed with `minMaintCredit` to reduce storage slot access
    address firstCollection;
    /// @notice user vip level related info
    /// 0 - 239 bits: store SafeBoxKey Count per vip level, per level using 24 bits
    /// 240 - 247 bits: store minMaintVipLevel
    /// 248 - 255 bits: remaining
    uint256 vipInfo;
    /// @notice Locked Credit amount which cannot be withdrawn and will be released as time goes.
    uint256 lockedCredit;
    mapping(address => CollectionAccount) accounts;
    mapping(address => uint256) tokenAmounts;
}

struct SafeBoxKey {
    /// locked credit amount of this safebox
    uint96 lockingCredit;
    /// corresponding key id of the safebox
    uint64 keyId;
    /// which vip level the safebox locked
    uint8 vipLevel;
}

struct CollectionAccount {
    mapping(uint256 => SafeBoxKey) keys;
    /// total locking credit of all `keys` in this collection
    uint96 totalLockingCredit;
    /// track next collection as linked list
    address next;
}

/// Internal Structure
struct LockParam {
    address proxyCollection;
    address collection;
    uint256[] nftIds;
    uint256 expiryTs;
    uint256 vipLevel;
    uint256 maxCreditCost;
    address creditToken;
}

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

import "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";

import "../Constants.sol";
import "../Errors.sol";
import {UserFloorAccount, CollectionAccount, SafeBoxKey} from "./Structs.sol";

library UserLib {
    using SafeCast for uint256;

    /// @notice update the account maintain credit on behalfOf `onBehalfOf`
    event UpdateMaintainCredit(address indexed onBehalfOf, uint256 minMaintCredit);

    address internal constant LIST_GUARD = address(1);

    function ensureVipCredit(UserFloorAccount storage account, uint8 requireVipLevel, address creditToken)
        internal
        view
        returns (uint256)
    {
        uint256 totalCredit = tokenBalance(account, creditToken);
        if (Constants.getVipBalanceRequirements(requireVipLevel) > totalCredit) {
            revert Errors.InsufficientBalanceForVipLevel();
        }
        return totalCredit;
    }

    function getMinMaintVipLevel(UserFloorAccount storage account) internal view returns (uint8) {
        unchecked {
            return uint8(account.vipInfo >> 240);
        }
    }

    function getMinLevelAndVipKeyCounts(uint256 vipInfo)
        internal
        pure
        returns (uint8 minLevel, uint256[] memory counts)
    {
        unchecked {
            counts = new uint256[](Constants.VIP_LEVEL_COUNT);
            minLevel = uint8(vipInfo >> 240);
            for (uint256 i; i < Constants.VIP_LEVEL_COUNT; ++i) {
                counts[i] = (vipInfo >> (i * 24)) & 0xFFFFFF;
            }
        }
    }

    function storeMinLevelAndVipKeyCounts(
        UserFloorAccount storage account,
        uint8 minMaintVipLevel,
        uint256[] memory keyCounts
    ) internal {
        unchecked {
            uint256 _data = (uint256(minMaintVipLevel) << 240);
            for (uint256 i; i < Constants.VIP_LEVEL_COUNT; ++i) {
                _data |= ((keyCounts[i] & 0xFFFFFF) << (i * 24));
            }
            account.vipInfo = _data;
        }
    }

    function getOrAddCollection(UserFloorAccount storage user, address collection)
        internal
        returns (CollectionAccount storage)
    {
        CollectionAccount storage entry = user.accounts[collection];
        if (entry.next == address(0)) {
            if (user.firstCollection == address(0)) {
                user.firstCollection = collection;
                entry.next = LIST_GUARD;
            } else {
                entry.next = user.firstCollection;
                user.firstCollection = collection;
            }
        }
        return entry;
    }

    function removeCollection(UserFloorAccount storage userAccount, address collection, address prev) internal {
        CollectionAccount storage cur = userAccount.accounts[collection];
        if (cur.next == address(0)) revert Errors.InvalidParam();

        if (collection == userAccount.firstCollection) {
            if (cur.next == LIST_GUARD) {
                userAccount.firstCollection = address(0);
            } else {
                userAccount.firstCollection = cur.next;
            }
        } else {
            CollectionAccount storage prevAccount = userAccount.accounts[prev];
            if (prevAccount.next != collection) revert Errors.InvalidParam();
            prevAccount.next = cur.next;
        }

        delete userAccount.accounts[collection];
    }

    function getByKey(UserFloorAccount storage userAccount, address collection)
        internal
        view
        returns (CollectionAccount storage)
    {
        return userAccount.accounts[collection];
    }

    function addSafeboxKey(CollectionAccount storage account, uint256 nftId, SafeBoxKey memory key) internal {
        if (account.keys[nftId].keyId > 0) {
            revert Errors.SafeBoxKeyAlreadyExist();
        }

        account.keys[nftId] = key;
    }

    function removeSafeboxKey(CollectionAccount storage account, uint256 nftId) internal {
        delete account.keys[nftId];
    }

    function getByKey(CollectionAccount storage account, uint256 nftId) internal view returns (SafeBoxKey storage) {
        return account.keys[nftId];
    }

    function tokenBalance(UserFloorAccount storage account, address token) internal view returns (uint256) {
        return account.tokenAmounts[token];
    }

    function lockCredit(UserFloorAccount storage account, uint256 amount) internal {
        unchecked {
            account.lockedCredit += amount;
        }
    }

    function unlockCredit(UserFloorAccount storage account, uint256 amount) internal {
        unchecked {
            account.lockedCredit -= amount;
        }
    }

    function depositToken(UserFloorAccount storage account, address token, uint256 amount) internal {
        account.tokenAmounts[token] += amount;
    }

    function withdrawToken(UserFloorAccount storage account, address token, uint256 amount, bool isCreditToken)
        internal
    {
        uint256 balance = account.tokenAmounts[token];
        if (balance < amount) {
            revert Errors.InsufficientCredit();
        }

        if (isCreditToken) {
            uint256 avaiableBuf;
            unchecked {
                avaiableBuf = balance - amount;
            }
            if (
                avaiableBuf < Constants.getVipBalanceRequirements(getMinMaintVipLevel(account))
                    || avaiableBuf < account.minMaintCredit || avaiableBuf < account.lockedCredit
            ) {
                revert Errors.InsufficientCredit();
            }

            account.tokenAmounts[token] = avaiableBuf;
        } else {
            unchecked {
                account.tokenAmounts[token] = balance - amount;
            }
        }
    }

    function transferToken(
        UserFloorAccount storage from,
        UserFloorAccount storage to,
        address token,
        uint256 amount,
        bool isCreditToken
    ) internal {
        withdrawToken(from, token, amount, isCreditToken);
        depositToken(to, token, amount);
    }

    function updateVipKeyCount(UserFloorAccount storage account, uint8 vipLevel, int256 diff) internal {
        if (vipLevel > 0 && diff != 0) {
            (uint8 minMaintVipLevel, uint256[] memory keyCounts) = getMinLevelAndVipKeyCounts(account.vipInfo);

            if (diff < 0) {
                keyCounts[vipLevel] -= uint256(-diff);
                if (vipLevel == minMaintVipLevel && keyCounts[vipLevel] == 0) {
                    uint8 newVipLevel = vipLevel;
                    do {
                        unchecked {
                            --newVipLevel;
                        }
                    } while (newVipLevel > 0 && keyCounts[newVipLevel] == 0);

                    minMaintVipLevel = newVipLevel;
                }
            } else {
                keyCounts[vipLevel] += uint256(diff);
                if (vipLevel > minMaintVipLevel) {
                    minMaintVipLevel = vipLevel;
                }
            }
            storeMinLevelAndVipKeyCounts(account, minMaintVipLevel, keyCounts);
        }
    }

    function recalculateMinMaintCredit(UserFloorAccount storage account, address onBehalfOf)
        public
        returns (uint256 maxLocking)
    {
        address prev = account.firstCollection;
        for (address collection = account.firstCollection; collection != LIST_GUARD && collection != address(0);) {
            (uint256 locking, address next) =
                (getByKey(account, collection).totalLockingCredit, getByKey(account, collection).next);
            if (locking == 0) {
                removeCollection(account, collection, prev);
                collection = next;
            } else {
                if (locking > maxLocking) {
                    maxLocking = locking;
                }
                prev = collection;
                collection = next;
            }
        }

        account.minMaintCredit = uint96(maxLocking);

        emit UpdateMaintainCredit(onBehalfOf, maxLocking);
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):