S Price: $0.565974 (-5.19%)

Contract Diff Checker

Contract Name:
PacaFinanceWithBoostAndScheduleUSDC

Contract Source Code:

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

pragma solidity ^0.8.20;

/**
 * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
 * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
 * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
 * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
 *
 * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
 * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
 * case an upgrade adds a module that needs to be initialized.
 *
 * For example:
 *
 * [.hljs-theme-light.nopadding]
 * ```solidity
 * contract MyToken is ERC20Upgradeable {
 *     function initialize() initializer public {
 *         __ERC20_init("MyToken", "MTK");
 *     }
 * }
 *
 * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
 *     function initializeV2() reinitializer(2) public {
 *         __ERC20Permit_init("MyToken");
 *     }
 * }
 * ```
 *
 * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
 * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
 *
 * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
 * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
 *
 * [CAUTION]
 * ====
 * Avoid leaving a contract uninitialized.
 *
 * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
 * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
 * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * /// @custom:oz-upgrades-unsafe-allow constructor
 * constructor() {
 *     _disableInitializers();
 * }
 * ```
 * ====
 */
abstract contract Initializable {
    /**
     * @dev Storage of the initializable contract.
     *
     * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions
     * when using with upgradeable contracts.
     *
     * @custom:storage-location erc7201:openzeppelin.storage.Initializable
     */
    struct InitializableStorage {
        /**
         * @dev Indicates that the contract has been initialized.
         */
        uint64 _initialized;
        /**
         * @dev Indicates that the contract is in the process of being initialized.
         */
        bool _initializing;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;

    /**
     * @dev The contract is already initialized.
     */
    error InvalidInitialization();

    /**
     * @dev The contract is not initializing.
     */
    error NotInitializing();

    /**
     * @dev Triggered when the contract has been initialized or reinitialized.
     */
    event Initialized(uint64 version);

    /**
     * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
     * `onlyInitializing` functions can be used to initialize parent contracts.
     *
     * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any
     * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in
     * production.
     *
     * Emits an {Initialized} event.
     */
    modifier initializer() {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        // Cache values to avoid duplicated sloads
        bool isTopLevelCall = !$._initializing;
        uint64 initialized = $._initialized;

        // Allowed calls:
        // - initialSetup: the contract is not in the initializing state and no previous version was
        //                 initialized
        // - construction: the contract is initialized at version 1 (no reininitialization) and the
        //                 current contract is just being deployed
        bool initialSetup = initialized == 0 && isTopLevelCall;
        bool construction = initialized == 1 && address(this).code.length == 0;

        if (!initialSetup && !construction) {
            revert InvalidInitialization();
        }
        $._initialized = 1;
        if (isTopLevelCall) {
            $._initializing = true;
        }
        _;
        if (isTopLevelCall) {
            $._initializing = false;
            emit Initialized(1);
        }
    }

    /**
     * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
     * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
     * used to initialize parent contracts.
     *
     * A reinitializer may be used after the original initialization step. This is essential to configure modules that
     * are added through upgrades and that require initialization.
     *
     * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
     * cannot be nested. If one is invoked in the context of another, execution will revert.
     *
     * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
     * a contract, executing them in the right order is up to the developer or operator.
     *
     * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization.
     *
     * Emits an {Initialized} event.
     */
    modifier reinitializer(uint64 version) {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing || $._initialized >= version) {
            revert InvalidInitialization();
        }
        $._initialized = version;
        $._initializing = true;
        _;
        $._initializing = false;
        emit Initialized(version);
    }

    /**
     * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
     * {initializer} and {reinitializer} modifiers, directly or indirectly.
     */
    modifier onlyInitializing() {
        _checkInitializing();
        _;
    }

    /**
     * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
     */
    function _checkInitializing() internal view virtual {
        if (!_isInitializing()) {
            revert NotInitializing();
        }
    }

    /**
     * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
     * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
     * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
     * through proxies.
     *
     * Emits an {Initialized} event the first time it is successfully executed.
     */
    function _disableInitializers() internal virtual {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing) {
            revert InvalidInitialization();
        }
        if ($._initialized != type(uint64).max) {
            $._initialized = type(uint64).max;
            emit Initialized(type(uint64).max);
        }
    }

    /**
     * @dev Returns the highest version that has been initialized. See {reinitializer}.
     */
    function _getInitializedVersion() internal view returns (uint64) {
        return _getInitializableStorage()._initialized;
    }

    /**
     * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
     */
    function _isInitializing() internal view returns (bool) {
        return _getInitializableStorage()._initializing;
    }

    /**
     * @dev Returns a pointer to the storage namespace.
     */
    // solhint-disable-next-line var-name-mixedcase
    function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
        assembly {
            $.slot := INITIALIZABLE_STORAGE
        }
    }
}

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

pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";

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

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

    /// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard
    struct ReentrancyGuardStorage {
        uint256 _status;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;

    function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) {
        assembly {
            $.slot := ReentrancyGuardStorageLocation
        }
    }

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    function __ReentrancyGuard_init() internal onlyInitializing {
        __ReentrancyGuard_init_unchained();
    }

    function __ReentrancyGuard_init_unchained() internal onlyInitializing {
        ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
        $._status = NOT_ENTERED;
    }

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

    function _nonReentrantBefore() private {
        ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if ($._status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        $._status = ENTERED;
    }

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

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

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";

/**
 * @title IERC1363
 * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
 *
 * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
 * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
 */
interface IERC1363 is IERC20, IERC165 {
    /*
     * Note: the ERC-165 identifier for this interface is 0xb0202a11.
     * 0xb0202a11 ===
     *   bytes4(keccak256('transferAndCall(address,uint256)')) ^
     *   bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
     *   bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
     *   bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
     *   bytes4(keccak256('approveAndCall(address,uint256)')) ^
     *   bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
     */

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferAndCall(address to, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @param data Additional data with no specified format, sent in call to `to`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param from The address which you want to send tokens from.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferFromAndCall(address from, address to, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param from The address which you want to send tokens from.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @param data Additional data with no specified format, sent in call to `to`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
     * @param spender The address which will spend the funds.
     * @param value The amount of tokens to be spent.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function approveAndCall(address spender, uint256 value) external returns (bool);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
     * @param spender The address which will spend the funds.
     * @param value The amount of tokens to be spent.
     * @param data Additional data with no specified format, sent in call to `spender`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}

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

pragma solidity ^0.8.20;

import {IERC165} from "../utils/introspection/IERC165.sol";

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

pragma solidity ^0.8.20;

import {IERC20} from "../token/ERC20/IERC20.sol";

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Permit.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
 * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
 *
 * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
 * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
 * need to send a transaction, and thus is not required to hold Ether at all.
 *
 * ==== Security Considerations
 *
 * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature
 * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be
 * considered as an intention to spend the allowance in any specific way. The second is that because permits have
 * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should
 * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be
 * generally recommended is:
 *
 * ```solidity
 * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {
 *     try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}
 *     doThing(..., value);
 * }
 *
 * function doThing(..., uint256 value) public {
 *     token.safeTransferFrom(msg.sender, address(this), value);
 *     ...
 * }
 * ```
 *
 * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of
 * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also
 * {SafeERC20-safeTransferFrom}).
 *
 * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so
 * contracts should have entry points that don't rely on permit.
 */
interface IERC20Permit {
    /**
     * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
     * given ``owner``'s signed approval.
     *
     * IMPORTANT: The same issues {IERC20-approve} has related to transaction
     * ordering also apply here.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `deadline` must be a timestamp in the future.
     * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
     * over the EIP712-formatted function arguments.
     * - the signature must use ``owner``'s current nonce (see {nonces}).
     *
     * For more information on the signature format, see the
     * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
     * section].
     *
     * CAUTION: See Security Considerations above.
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    /**
     * @dev Returns the current nonce for `owner`. This value must be
     * included whenever a signature is generated for {permit}.
     *
     * Every successful call to {permit} increases ``owner``'s nonce by one. This
     * prevents a signature from being used multiple times.
     */
    function nonces(address owner) external view returns (uint256);

    /**
     * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
     */
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
import {Address} from "../../../utils/Address.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spender);
            if (currentAllowance < requestedDecrease) {
                revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
            }
            forceApprove(token, spender, currentAllowance - requestedDecrease);
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     *
     * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
     * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
     * set here.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

        if (!_callOptionalReturnBool(token, approvalCall)) {
            _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
            _callOptionalReturn(token, approvalCall);
        }
    }

    /**
     * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            safeTransfer(token, to, value);
        } else if (!token.transferAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
     * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferFromAndCallRelaxed(
        IERC1363 token,
        address from,
        address to,
        uint256 value,
        bytes memory data
    ) internal {
        if (to.code.length == 0) {
            safeTransferFrom(token, from, to, value);
        } else if (!token.transferFromAndCall(from, to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
     * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
     * once without retrying, and relies on the returned value to be true.
     *
     * Reverts if the returned value is other than `true`.
     */
    function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            forceApprove(token, to, value);
        } else if (!token.approveAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            // bubble errors
            if iszero(success) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            returnSize := returndatasize()
            returnValue := mload(0)
        }

        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
     */
    function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        bool success;
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0)
        }
        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
    }
}

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

pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev There's no code at `target` (it is not a contract).
     */
    error AddressEmptyCode(address target);

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        if (address(this).balance < amount) {
            revert Errors.InsufficientBalance(address(this).balance, amount);
        }

        (bool success, ) = recipient.call{value: amount}("");
        if (!success) {
            revert Errors.FailedCall();
        }
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason or custom error, it is bubbled
     * up by this function (like regular Solidity function calls). However, if
     * the call reverted with no returned reason, this function reverts with a
     * {Errors.FailedCall} error.
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata);
    }

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
     * was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case
     * of an unsuccessful call.
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata
    ) internal view returns (bytes memory) {
        if (!success) {
            _revert(returndata);
        } else {
            // only check if target is a contract if the call was successful and the return data is empty
            // otherwise we already know that it was a contract
            if (returndata.length == 0 && target.code.length == 0) {
                revert AddressEmptyCode(target);
            }
            return returndata;
        }
    }

    /**
     * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the
     * revert reason or with a default {Errors.FailedCall} error.
     */
    function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
        if (!success) {
            _revert(returndata);
        } else {
            return returndata;
        }
    }

    /**
     * @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedCall}.
     */
    function _revert(bytes memory returndata) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            assembly ("memory-safe") {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert Errors.FailedCall();
        }
    }
}

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

pragma solidity ^0.8.20;

/**
 * @dev Collection of common custom errors used in multiple contracts
 *
 * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
 * It is recommended to avoid relying on the error API for critical functionality.
 *
 * _Available since v5.1._
 */
library Errors {
    /**
     * @dev The ETH balance of the account is not enough to perform the operation.
     */
    error InsufficientBalance(uint256 balance, uint256 needed);

    /**
     * @dev A call to an address target failed. The target may have reverted.
     */
    error FailedCall();

    /**
     * @dev The deployment failed.
     */
    error FailedDeployment();

    /**
     * @dev A necessary precompile is missing.
     */
    error MissingPrecompile(address);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC-165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[ERC].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

pragma solidity ^0.8.20;

interface iPriceOracle {
    // returns price in USD
    function getLatestPrice(address token) external view returns (uint256);
}

// File: paca.sol

contract PacaFinanceWithBoostAndScheduleUSDC is Initializable, ReentrancyGuardUpgradeable {
    using SafeERC20 for IERC20;


    // Struct Definitions
    struct Pool {
        uint256 lockupPeriod;
        uint256 dailyRewardRate;
        uint256 totalStaked;
        uint256 totalRewards;
        address tokenAddress;
    }

    struct Stake {
        uint256 amount;
        uint256 lastClaimed;
        uint256 dailyRewardRate;
        uint256 unlockTime;
        bool complete;
    }

    struct StakeInput {
        address user;
        uint256 amount;
        uint256 lastClaimed;
        uint256 unlockTime;
        uint256 dailyRewardRate;
    }

    struct Vesting {
        uint256 amount;
        uint256 bonus;
        uint256 lockedUntil;
        uint256 claimedAmount;
        uint256 claimedBonus;
        uint256 lastClaimed;
        uint256 createdAt;
        address token;
        bool complete;
        uint256 usdAmount;
    }

    struct UnlockStep {
        uint256 timeOffset;
        uint256 percentage;
    }

    struct BoostRange {
        uint256 minTokens;
        uint256 maxTokens;
        uint256 boostPercentage;
    }

    struct WithdrawStake {
        uint256 stakeId;
        uint256 amount;
        uint256 unlockTime;
    }

    struct SellStake {
        uint256 price;
        uint256 bonusAmount;
        uint256 amount;
        uint256 lastClaimed;
        uint256 dailyRewardRate;
    }

    BoostRange[] public boosttiers;

    // Contract Variables
    Pool public pool;
    address public owner;
    mapping(address => bool) public owners;
    mapping(address => Stake[]) public stakes;
    mapping(address => Vesting[]) public vestings;
    mapping(address => UnlockStep[]) public unlockSchedules;
    mapping(address => address) public priceOracles;
    mapping(address => uint256) public dollarsVested; // per user address
    uint256 public lockupDuration;
    uint256 public minStakeLock;
    uint256 private constant BONUS_PERCENTAGE = 10;

    mapping(address => bool) public authorizedBots;
    mapping(address => uint256) public vestedTotal; // per vesting token
    uint256 public unlockDelay;
    uint256 public withdrawLiabilities;
    mapping(address => WithdrawStake[]) public withdrawStake;
    uint256 public restakeBonus;
    mapping(address => uint256) public addressFixedRate;
    mapping(address => mapping(uint256 => SellStake)) public sellStakes;
    uint256 public sellTax;
    uint256 public sellKickBack;

    // Events
    event Staked(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);
    event VestingCreated(address indexed user, uint256 amount, uint256 bonus);
    event VestingClaimed(address indexed user, uint256 amount, uint256 bonus);
    event BonusClaimed(address indexed user, uint256 bonus);
    event PoolUpdated(uint256 lockupPeriod, uint256 dailyRewardRate);
    event UnlockScheduleSet(address indexed token);
    event FundsWithdrawn(address indexed owner, address indexed token, uint256 amount);
    event RewardsDeposited(uint256 amount);
    event CompoundRewards(address indexed user, uint256 amount);
    event MinStakeLockUpdated(uint256 amount);
    event StakeWithdrawn(address indexed user, uint256 amount, uint256 stakeId);
    event StakeUpForSale(address indexed user, uint256 saleAmount, uint256 stakeId);
    event StakeSaleCancelled(address indexed user, uint256 stakeId);
    event StakeSold(address indexed seller,  address indexed buyer,uint256 saleAmount, uint256 stakeId);

    // Modifiers
    modifier onlyOwner() {
        require(owners[msg.sender], "Not authorized");
        _;
    }
    modifier onlyBot() {
        require(authorizedBots[msg.sender], "Caller is not an authorized bot");
        _;
    }

    function initialize() public initializer {
        __ReentrancyGuard_init(); // Initialize ReentrancyGuardUpgradeable
        owner = 0x41970Ce76b656030A79E7C1FA76FC4EB93980255;
        owners[0x41970Ce76b656030A79E7C1FA76FC4EB93980255] = true;

        lockupDuration = 250 days;
        minStakeLock = 16 ether;

        pool.tokenAddress = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
        pool.lockupPeriod = 250 * 1 days;
        pool.dailyRewardRate = 33;

        // Push boost tiers in ascending order (amounts in wei, boosts as percentages)
        boosttiers.push(BoostRange(3500 * 1e18, 9999 * 1e18, 1));        // $3,500 to $9,999 = 0.01 boost
        boosttiers.push(BoostRange(10000 * 1e18, 19999 * 1e18, 2));      // $10,000 to $19,999 = 0.02 boost
        boosttiers.push(BoostRange(20000 * 1e18, 29999 * 1e18, 3));      // $20,000 to $29,999 = 0.03 boost
        boosttiers.push(BoostRange(30000 * 1e18, 39999 * 1e18, 4));      // $30,000 to $39,999 = 0.04 boost
        boosttiers.push(BoostRange(40000 * 1e18, 59999 * 1e18, 5));      // $40,000 to $59,999 = 0.05 boost
        boosttiers.push(BoostRange(60000 * 1e18, 79999 * 1e18, 6));      // $60,000 to $79,999 = 0.06 boost
        boosttiers.push(BoostRange(80000 * 1e18, 99999 * 1e18, 8));      // $80,000 to $99,999 = 0.08 boost
        boosttiers.push(BoostRange(100000 * 1e18, type(uint256).max, 10)); // $100,000+ = 0.1 boost

        // Price oracle for a specific tokens
        // priceOracles[0x940181a94A35A4569E4529A3CDfB74e38FD98631] = 0x0Dde1b42F7B3891C9731280A74081501729A73c5;
        authorizedBots[0xbf12D3b827a230F7390EbCc9b83b289FdC98ba81] = true;
        authorizedBots[0x7c40f272570fdf9549d6f67493aC250a1DB52F27] = true;

        unlockDelay = 60 * 60 * 36;
        restakeBonus = 3;
    }

    // Ownership Management
    function addOwner(address _newOwner) external onlyOwner {
        require(!owners[_newOwner], "Already an owner");
        owners[_newOwner] = true;
    }

    function removeOwner(address _owner) external onlyOwner {
        require(owners[_owner], "Not an owner");
        require(_owner != msg.sender, "Cannot remove yourself");
        owners[_owner] = false;
    }

    /// @notice Function to add a bot to the list (only callable by the contract owner)
    function addBot(address bot) external onlyOwner {
        require(bot != address(0), "Invalid address");
        authorizedBots[bot] = true;
    }

    /// @notice Function to remove a bot from the list (only callable by the contract owner)
    function removeBot(address bot) external onlyOwner {
        require(bot != address(0), "Invalid address");
        authorizedBots[bot] = false;
    }

    // Admin Functions
    function updatePool(uint256 _lockupPeriod, uint256 _dailyRewardRate) external onlyOwner {
        pool.lockupPeriod = _lockupPeriod * 1 days;
        pool.dailyRewardRate = _dailyRewardRate;
        emit PoolUpdated(_lockupPeriod, _dailyRewardRate);
    }

    function depositRewards(uint256 _amount) external onlyOwner {
        IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);
        pool.totalRewards = pool.totalRewards + _amount;
        emit RewardsDeposited(_amount);
    }

    function updateStakeMin(uint256 _amount) external onlyOwner {
        minStakeLock = _amount;
        emit MinStakeLockUpdated(_amount);
    }

    function updateUnlockDelay(uint256 _delay) external onlyOwner {
        unlockDelay = _delay;
    }

    function updateRestakeBonus(uint256 _newBonus) external onlyOwner {
        restakeBonus = _newBonus;
    }
        
    /// @notice New Stake Sell Tax
    /// @param _newTax The rate expressed in 2 digits, ex: 20
    function updateSellTax(uint256 _newTax) external onlyOwner {
        sellTax = _newTax;
    }

    /// @notice New Stake Sell kickback to the buyer
    /// @param _newKickback The rate expressed in 2 digits, ex: 5
    function updatesellKickBack(uint256 _newKickback) external onlyOwner {
        sellKickBack = _newKickback;
    }

    /// @notice Function to add an address to have a fixed daily reward (only callable by the contract owner)
    /// @param _addr The address to give a fixed rate
    /// @param _rate The fixed rate expressed in 2 digits, ex: 40
    function addFixedRate(address _addr, uint _rate) external onlyOwner {
        require(_addr != address(0), "Invalid address");
        addressFixedRate[_addr] = _rate;
    }

    /// @notice Function to remove an address' fixed daily reward (only callable by the contract owner)
    /// @param _addr The address to 0 out
    function removeFixedRate(address _addr) external onlyOwner {
        require(_addr != address(0), "Invalid address");
        addressFixedRate[_addr] = 0;
    }

    /// @notice Add or edit a tier range
    function addOrEditTier(uint256 minTokens, uint256 maxTokens, uint256 boostPercentage) public onlyOwner {
        require(minTokens < maxTokens, "Invalid range: minTokens must be < maxTokens");
        require(!rangesOverlap(minTokens, maxTokens), "Range overlaps with existing tiers");

        // Check if editing an existing range
        for (uint256 i = 0; i < boosttiers.length; ++i) {
            if (boosttiers[i].minTokens == minTokens && boosttiers[i].maxTokens == maxTokens) {
                // Edit the existing range
                boosttiers[i].boostPercentage = boostPercentage;
                return;
            }
        }

        // Add new range
        boosttiers.push(BoostRange(minTokens, maxTokens, boostPercentage));

        // Sort the ranges after adding
        sortRanges();
    }

    // Check for range overlap
    function rangesOverlap(uint256 minTokens, uint256 maxTokens) internal view returns (bool) {
        for (uint256 i = 0; i < boosttiers.length; ++i) {
            if (minTokens <= boosttiers[i].maxTokens && maxTokens >= boosttiers[i].minTokens) {
                return true;
            }
        }
        return false;
    }

    /// @notice Sort ranges by minTokens
    function sortRanges() internal {
        for (uint256 i = 0; i < boosttiers.length; ++i) {
            for (uint256 j = i + 1; j < boosttiers.length; j++) {
                if (boosttiers[i].minTokens > boosttiers[j].minTokens) {
                    // Swap ranges
                    BoostRange memory temp = boosttiers[i];
                    boosttiers[i] = boosttiers[j];
                    boosttiers[j] = temp;
                }
            }
        }
    }

    /// @notice Remove a range by index
    function removeTier(uint256 index) external onlyOwner {
        require(index < boosttiers.length, "Index out of bounds");
        for (uint256 i = index; i < boosttiers.length - 1; ++i) {
            boosttiers[i] = boosttiers[i + 1];
        }
        boosttiers.pop();
    }

    function withdrawFromStakingPool(uint256 _amount) external onlyOwner {
        IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount);
        emit FundsWithdrawn(msg.sender, pool.tokenAddress, _amount);
    }

    function withdrawFromVestingPool(address _token, uint256 _amount) external onlyOwner {
        IERC20(_token).safeTransfer(msg.sender, _amount);
        emit FundsWithdrawn(msg.sender, _token, _amount);
    }

    function setUnlockScheduleByPercentage(
        address _token,
        uint256 _lockTime, // Total lock time in seconds
        uint256 _percentagePerStep // Percentage unlocked per step (in basis points, e.g., 100 = 1%)
    ) external onlyOwner {
        require(_lockTime != 0, "Lock time must be greater than zero");
        require(_percentagePerStep != 0, "Percentage per step must be greater than zero");

        uint256 totalPercentage = 10000; // 100% in basis points
        require(totalPercentage % _percentagePerStep == 0, "Percentage must divide 100% evenly");

        uint256 steps = totalPercentage / _percentagePerStep; // Number of steps
        uint256 stepTime = _lockTime / steps; // Time interval per step

        delete unlockSchedules[_token]; // Clear existing schedule for this token

        for (uint256 i = 1; i <= steps; ++i) {
            unlockSchedules[_token].push(UnlockStep({
                timeOffset: stepTime * i, // Time offset for this step
                percentage: _percentagePerStep
            }));
        }

        emit UnlockScheduleSet(_token);
    }

    /// @notice Get the boost percentage for a given token amount
    function getBoost(uint256 depositedTokens) public view returns (uint256) {
        for (uint256 i = 0; i < boosttiers.length; ++i) {
            if (depositedTokens >= boosttiers[i].minTokens && depositedTokens <= boosttiers[i].maxTokens) {
                return boosttiers[i].boostPercentage;
            }
        }
        return 0; // Default boost if no range matches
    }

    /// @notice This function will end and clear a user's stakes.
    /// @dev Only to be used by bots in emergencies
    /// @param user The user whose stakes will be ended and 0'd
    function clearStakes(address user) external onlyBot {
        uint256 clearedStakes = 0;

        for (uint256 i = 0; i < stakes[user].length; ++i) {
            Stake storage stake = stakes[user][i];
            clearedStakes = clearedStakes + stake.amount;
            stake.amount = 0;
            stake.complete = true;
        }

        pool.totalStaked = pool.totalStaked - clearedStakes;
    }

    /// @notice This function will end and clear a user's withdraw stakes.
    /// @dev Only to be used by bots in emergencies
    /// @param user The user whose withdraw stakes will be 0'd
    function clearWithdrawStakes(address user) external onlyBot {
        uint256 clearedStakes = 0;

        for (uint256 i = 0; i < withdrawStake[user].length; ++i) {
            WithdrawStake storage stake = withdrawStake[user][i];
            clearedStakes = clearedStakes + stake.amount;
            stake.amount = 0;
        }

        withdrawLiabilities -= clearedStakes;
    }

    function createStake(uint256 _amount) external {
        // Scale up for wei comparison, USDC is 1e6
        require(_amount * 1e12 > minStakeLock, "Amount must be greater minStakeLock");

        // Transfer tokens from the user into the contract
        IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), _amount);

        // Check if user has a fixed reward rate set
        uint256 finalRewardRate;
        if (addressFixedRate[msg.sender] > 0) {
            // Use the fixed rate
            finalRewardRate = addressFixedRate[msg.sender];
        } else {
            // Default logic, restake = false
            finalRewardRate = getUserRewardRate(msg.sender, false);
        }

        // Create the stake
        stakes[msg.sender].push(Stake({
            amount: _amount,
            lastClaimed: block.timestamp,
            dailyRewardRate: finalRewardRate,
            unlockTime: block.timestamp + pool.lockupPeriod,
            complete: false
        }));

        // Update total staked
        pool.totalStaked += _amount;

        emit Staked(msg.sender, _amount);
    }


    /// @notice Restake an expired stake with a bonus daily reward
    function restake(uint256 _stakeIndex, uint256 _restakePercentage) nonReentrant external {
        require(_restakePercentage <= 100, "Invalid restake percentage");
        Stake storage stake = stakes[msg.sender][_stakeIndex];
        // Ensure there is a stake to claim
        require(stake.amount != 0, "No amount to claim");
        require(block.timestamp >= stake.unlockTime, "Stake is still locked");

        uint256 _amount = stake.amount;
        uint rewards = getPoolRewards(msg.sender, _stakeIndex);
        _amount = _amount + rewards;

        uint256 restake_amount = (_amount * _restakePercentage) / 100;
        uint256 withdraw_amount = _amount - restake_amount;

        // Update state before external calls
        stake.amount = 0;
        stake.complete = true;

        // Process withdraw
        if (withdraw_amount > 0) {
            withdrawLiabilities += withdraw_amount;

            if (pool.totalStaked >= withdraw_amount) {
                pool.totalStaked -= withdraw_amount;
            } else {
                pool.totalStaked = 0;
            }
            // Create temporary the stake for the user to delay withdraw
            withdrawStake[msg.sender].push(WithdrawStake({
                stakeId: _stakeIndex,
                amount: withdraw_amount,
                unlockTime: block.timestamp + unlockDelay
                }));

            // Emit a detailed event
            emit RewardClaimed(msg.sender, withdraw_amount);

        }
        // Process restake
        if (restake_amount > 0) {
            // Check if user has a fixed reward rate set
            uint256 finalRewardRate;
            if (addressFixedRate[msg.sender] > 0) {
                // Use the fixed rate
                finalRewardRate = addressFixedRate[msg.sender];
            } else {
                // restake = true
                finalRewardRate = getUserRewardRate(msg.sender, true);
            }

            stakes[msg.sender].push(Stake({
                amount: restake_amount,
                lastClaimed: block.timestamp,
                dailyRewardRate: finalRewardRate,
                unlockTime: block.timestamp + pool.lockupPeriod,
                complete: false
            }));

            emit Staked(msg.sender, restake_amount);
        }
    }

    function createStakeForUser(address _user, uint256 _amount) external onlyOwner {
        require(_amount != 0, "Invalid amount");

        stakes[_user].push(Stake({
            amount: _amount,
            lastClaimed: block.timestamp,
            dailyRewardRate: pool.dailyRewardRate,
            unlockTime: block.timestamp + pool.lockupPeriod,
            complete: false
        }));

        pool.totalStaked = pool.totalStaked + _amount;
        emit Staked(_user, _amount);
    }


    function createStakes(StakeInput[] calldata stakesInput) external onlyBot payable {
        uint256 totalLength = stakesInput.length;

        for (uint256 i; i < totalLength;) {
            StakeInput calldata stakeInput = stakesInput[i];

            // Update pool total
            pool.totalStaked = pool.totalStaked + stakeInput.amount;

            // Create the stake for the user
            stakes[stakeInput.user].push(Stake({
                amount: stakeInput.amount,
                lastClaimed: stakeInput.lastClaimed,
                dailyRewardRate: stakeInput.dailyRewardRate,
                unlockTime: stakeInput.unlockTime,
                complete: false
            }));

            unchecked {
                ++i;
            }
        }
    }

    function getPoolRewards(address _user, uint _stakeIndex) public view returns (uint256) {
        Stake storage stake = stakes[_user][_stakeIndex];
        uint256 elapsedTime = block.timestamp - stake.lastClaimed;
        uint256 rewards = (stake.amount * stake.dailyRewardRate * elapsedTime) / 1 days / 10000;

        return rewards;
    }

    function getUserRewardRate(address _user, bool isRestake) public view returns (uint256) {
        uint256 finalRewardRate = pool.dailyRewardRate +  getBoost(dollarsVested[_user]);
        if (isRestake) {
            finalRewardRate += restakeBonus;
        }
        return finalRewardRate;
    }

    function claimRewards() external nonReentrant {
        uint256 totalReward = 0;

        for (uint256 i = 0; i < stakes[msg.sender].length; ++i) {
            Stake storage stake = stakes[msg.sender][i];
            if (stake.amount > 0) {
                uint rewards = getPoolRewards(msg.sender, i);
                totalReward = totalReward + rewards;
                stake.lastClaimed = block.timestamp;
            }
        }

        require(totalReward != 0, "No rewards to claim");
        require(pool.totalRewards >= totalReward, "Insufficient rewards in the pool");

        pool.totalRewards = pool.totalRewards - totalReward;
        IERC20(pool.tokenAddress).safeTransfer(msg.sender, totalReward);

        emit RewardClaimed(msg.sender, totalReward);
    }


    function claimStake(uint256 _stakeIndex) external nonReentrant {
        // Ensure the stake index is valid
        require(_stakeIndex < stakes[msg.sender].length, "Invalid stake index");

        // Load the stake
        Stake storage stake = stakes[msg.sender][_stakeIndex];
        uint256 _amount = stake.amount;

        uint rewards = getPoolRewards(msg.sender, _stakeIndex);

        _amount = _amount + rewards;

        // Ensure there is a stake to claim
        require(_amount != 0, "No amount to claim");

        // Ensure the stake is unlocked (if using lockup periods)
        require(block.timestamp >= stake.unlockTime, "Stake is still locked");

        // Update state before external calls
        stake.amount = 0;
        stake.complete = true;
        withdrawLiabilities += _amount;

        if (pool.totalStaked >= _amount) {
            pool.totalStaked -= _amount;
        } else {
            pool.totalStaked = 0;
        }

        // Create temporary the stake for the user to delay withdraw
        withdrawStake[msg.sender].push(WithdrawStake({
            stakeId: _stakeIndex,
            amount: _amount,
            unlockTime: block.timestamp + unlockDelay
            }));

        // Emit a detailed event
        emit RewardClaimed(msg.sender, _amount);
    }

    /**
     * @notice Withdraw a staked amount after its unlock time has passed.
     * @dev Locates the stake by `_stakeIndex`, checks that it's unlocked and non-zero,
     * and transfers tokens to the caller. For vesting stakes (where `_stakeIndex` >= 1e6),
     * the stored amount (in 1e18 decimals) is scaled to USDC's 1e6 decimals by dividing by 1e12.
     *
     * Requirements:
     * - Caller must have at least one stake.
     * - The stake must exist, be unlocked, and have a non-zero amount.
     * - The contract must have sufficient token balance.
     *
     * @param _stakeIndex The identifier of the stake to withdraw.
     */
    function withdraw(uint256 _stakeIndex) external nonReentrant {
        uint256 _amount = 0;
        WithdrawStake[] storage userStakes = withdrawStake[msg.sender];
        require(userStakes.length > 0, "No stakes available for withdrawal");

        for (uint256 i = 0; i < userStakes.length; ++i) {
            WithdrawStake storage stake = userStakes[i];
            if (stake.stakeId == _stakeIndex) {
                require(stake.amount != 0, "Stake already withdrawn or does not exist");
                require(block.timestamp >= stake.unlockTime, "Withdraw Stake is still locked");

                _amount = stake.amount;

                // Convert vesting stake amount to USDC decimals.
                if (_stakeIndex >= 1e6) {
                    _amount = _amount / 1e12;
                }

                uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this));
                require(poolBalance >= _amount, "Insufficient rewards in the pool");


                // Update state before external calls
                // withdrawLiabilities is in 1e18, deduct original amount
                withdrawLiabilities -= stake.amount;
                stake.amount = 0;

                // Transfer tokens
                IERC20(pool.tokenAddress).safeTransfer(msg.sender, _amount);


                emit StakeWithdrawn(msg.sender, _amount, _stakeIndex);
                return;
            }
        }

        // Revert if no matching stake was found
        revert("Invalid stake index");
    }


    function compoundAllRewards() external {
        uint256 totalReward = 0;

        for (uint256 i = 0; i < stakes[msg.sender].length; ++i) {
            Stake storage stake = stakes[msg.sender][i];
            if (stake.amount > 0) {
                uint rewards = getPoolRewards(msg.sender, i);
                totalReward = totalReward + rewards;
                stake.lastClaimed = block.timestamp;
            }
        }

        require(totalReward > minStakeLock, "Not enough to compound");

        stakes[msg.sender].push(Stake({
            amount: totalReward,
            lastClaimed: block.timestamp,
            dailyRewardRate: pool.dailyRewardRate + getBoost(dollarsVested[msg.sender]),
            unlockTime: block.timestamp + pool.lockupPeriod,
            complete: false
        }));

        pool.totalStaked = pool.totalStaked + totalReward;
        emit CompoundRewards(msg.sender, totalReward);
    }

    function createVesting(address _token, uint256 _amount) external {
        require(_amount != 0, "Amount must be greater than zero");
        address oracle = priceOracles[_token];
        require(oracle != address(0), "Price oracle not set for this token");
        IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);

        uint256 bonus = (_amount * BONUS_PERCENTAGE) / 100;

        uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * _amount) / 1e18;
        require(usdPrice > minStakeLock, "Amount must be greater minStakeLock");

        // Update user's dollarsVested
        dollarsVested[msg.sender] += usdPrice;
        // Update token's vestedTotal
        vestedTotal[_token] += _amount;


        vestings[msg.sender].push(Vesting({
            amount: _amount,
            bonus: bonus,
            lockedUntil: block.timestamp + lockupDuration,
            claimedAmount: 0,
            claimedBonus: 0,
            lastClaimed: block.timestamp,
            createdAt: block.timestamp,
            token: _token,
            complete: false,
            usdAmount: usdPrice
        }));

        emit VestingCreated(msg.sender, _amount, bonus);
    }

    function getUnlockedVesting(address _user, uint256 _vestingIndex) public view returns (uint256) {
        Vesting storage vesting = vestings[_user][_vestingIndex];
        uint256 timeElapsed = block.timestamp - vesting.createdAt;
        address token = vesting.token;

        uint256 unlockedAmount = 0;

        for (uint256 i = 0; i < unlockSchedules[token].length; ++i) {
            UnlockStep storage step = unlockSchedules[token][i];
            uint256 timeTier = step.timeOffset;
            uint256 percentage = step.percentage;

            if (timeElapsed >= timeTier) {
                unlockedAmount = unlockedAmount + ((vesting.amount * percentage) / 10000);
            }
        }

        return unlockedAmount;
    }

    function getVestingSchedule(address _user, uint256 _vestingIndex) public view returns (uint256[] memory, uint256[] memory) {
        Vesting storage vesting = vestings[_user][_vestingIndex];
        address token = vesting.token;

        uint256 scheduleLength = unlockSchedules[token].length;
        uint256[] memory unlockTimestamps = new uint256[](scheduleLength);
        uint256[] memory unlockPercentages = new uint256[](scheduleLength);

        for (uint256 i = 0; i < scheduleLength; ++i) {
            UnlockStep storage step = unlockSchedules[token][i];

            // Calculate the absolute unlock timestamp
            unlockTimestamps[i] = vesting.createdAt + step.timeOffset;
            unlockPercentages[i] = step.percentage; // Percentage is stored as scaled by 10000 (e.g., 2500 = 25%)
        }

        return (unlockTimestamps, unlockPercentages);
    }

    function getUnlockedVestingBonus(address _user, uint256 _vestingIndex) public view returns (uint256) {
        Vesting storage vesting = vestings[_user][_vestingIndex];
        uint256 timeElapsed = block.timestamp - vesting.createdAt;
        address token = vesting.token;

        uint256 unlockedAmount = 0;

        for (uint256 i = 0; i < unlockSchedules[token].length; ++i) {
            UnlockStep storage step = unlockSchedules[token][i];
            uint256 timeTier = step.timeOffset;
            uint256 percentage = step.percentage;
            uint256 maxBonusAmount = (vesting.usdAmount * BONUS_PERCENTAGE) / 100;

            if (timeElapsed >= timeTier) {
                unlockedAmount = unlockedAmount + ((maxBonusAmount * percentage) / 10000);
            }
        }

        return unlockedAmount;
    }


    function claimVesting(uint256 _vestingIndex) external nonReentrant {
        Vesting storage vesting = vestings[msg.sender][_vestingIndex];
        require(vesting.complete == false, "Stake is Complete");
        uint256 maxClaim = getUnlockedVesting(msg.sender, _vestingIndex);

        require(maxClaim >= vesting.claimedAmount, "Invalid claim amount");
        uint256 amountToClaim = maxClaim - vesting.claimedAmount;
        require(amountToClaim != 0, "No vested amount to claim");

        vesting.claimedAmount = vesting.claimedAmount + amountToClaim;
        if (vesting.claimedAmount >= vesting.amount) {
            vesting.complete = true;
        }
        // Update user's dollarsVested
        if (dollarsVested[msg.sender] > 0) {
            uint256 usdPrice = (iPriceOracle(priceOracles[vesting.token]).getLatestPrice(vesting.token) * amountToClaim) / 1e18;
            if (usdPrice >= dollarsVested[msg.sender]) {
                dollarsVested[msg.sender] = 0;
            } else {
                dollarsVested[msg.sender] -= usdPrice;
            }
        }
        vestedTotal[vesting.token] -= amountToClaim;
        IERC20(vesting.token).safeTransfer(msg.sender, amountToClaim);

        emit VestingClaimed(msg.sender, amountToClaim, 0);
    }

    function claimAllVestingByToken(address _token) external nonReentrant {
        uint256 totalReward = 0;
        uint256 vestingsProcessed = 0;

        for (uint256 i = 0; i < vestings[msg.sender].length; ++i) {
            Vesting storage vesting = vestings[msg.sender][i];

            if (vesting.token == _token && !vesting.complete) {
                uint256 maxClaim = getUnlockedVesting(msg.sender, i);
                require(maxClaim >= vesting.claimedAmount, "Invalid claim amount");

                uint256 amountToClaim = maxClaim - vesting.claimedAmount;
                if (amountToClaim > 0) {
                    vesting.claimedAmount = vesting.claimedAmount + amountToClaim;
                    totalReward = totalReward + amountToClaim;
                    vesting.lastClaimed = block.timestamp;

                    // Mark vesting as complete if fully claimed
                    if (vesting.claimedAmount >= vesting.amount) {
                        vesting.complete = true;
                    }

                    vestingsProcessed++;
                }
            }
        }

        require(totalReward != 0, "No rewards to claim");

        // Update user's dollarsVested
        if (dollarsVested[msg.sender] > 0) {
            uint256 usdPrice = (iPriceOracle(priceOracles[_token]).getLatestPrice(_token) * totalReward) / 1e18;
            if (usdPrice >= dollarsVested[msg.sender]) {
                dollarsVested[msg.sender] = 0;
            } else {
                dollarsVested[msg.sender] -= usdPrice;
            }
        }

        // Ensure the contract has enough balance to fulfill the claim
        uint256 poolBalance = IERC20(_token).balanceOf(address(this));
        require(poolBalance >= totalReward, "Insufficient rewards in the pool");
        // Update vesting total
        vestedTotal[_token] -= totalReward;
        // Transfer the aggregated reward
        IERC20(_token).safeTransfer(msg.sender, totalReward);

        emit RewardClaimed(msg.sender, totalReward);
    }


    function claimBonus(uint256 _vestingIndex) external nonReentrant {
        Vesting storage vesting = vestings[msg.sender][_vestingIndex];
        uint256 maxBonus = getUnlockedVestingBonus(msg.sender, _vestingIndex);

        require(maxBonus >= vesting.claimedBonus, "Invalid claim amount");
        uint256 bonusToClaim = maxBonus - vesting.claimedBonus;
        require(bonusToClaim != 0, "No vested amount to claim");

        vesting.claimedBonus = vesting.claimedBonus + bonusToClaim;
        withdrawLiabilities += bonusToClaim;

        // IERC20(vesting.token).safeTransfer(msg.sender, bonusToClaim);

        // Create temporary the stake for the user to delay withdraw.
        // Add 1e6 to the vesting index to distinguish them from normal stakes.
        withdrawStake[msg.sender].push(WithdrawStake({
            stakeId: _vestingIndex + 1e6,
            amount: bonusToClaim,
            unlockTime: block.timestamp + unlockDelay
            }));

        emit BonusClaimed(msg.sender, bonusToClaim);
    }

    function setPriceOracle(address _token, address _oracle) external onlyOwner {
        priceOracles[_token] = _oracle;
    }

    function viewRewards(address _user) external view returns (uint256) {
        uint256 totalReward = 0;

        for (uint256 i = 0; i < stakes[_user].length; ++i) {
            uint rewards = getPoolRewards(_user, i);
            totalReward = totalReward + rewards;
        }

        return totalReward;
    }

    /// @notice View function to get all stakes for a specific address
    function getStakes(address user) external view returns (Stake[] memory) {
        return stakes[user];
    }

    /// @notice View function to get all vestings for a specific address
    function getVestings(address user) external view returns (Vesting[] memory) {
        return vestings[user];
    }

    /// @notice View to monitor contract pool deficits
    function getPoolStatus() external view returns (uint256) {
        uint256 poolBalance = IERC20(pool.tokenAddress).balanceOf(address(this));

        // If the balance is greater than or equal to liabilities, return 0
        if (poolBalance >= withdrawLiabilities) {
            return 0;
        }

        // Otherwise, return the deficit (amount needed to cover liabilities)
        return withdrawLiabilities - poolBalance;
    }

    /**
     * @notice Returns the vested amounts and USD values for an array of tokens.
     * @param _tokens The array of token addresses to evaluate.
     * @return amounts The array of vested amounts for each token.
     * @return usdValues The array of USD values for each token’s vested amount.
     * @return totalUsd The total USD value of all vested tokens in the array.
     */
    function getVestedTotals(address[] calldata _tokens)
        external
        view
        returns (
            uint256[] memory amounts,
            uint256[] memory usdValues,
            uint256 totalUsd
        )
    {
        uint256 length = _tokens.length;
        amounts = new uint256[](length);
        usdValues = new uint256[](length);

        for (uint256 i = 0; i < length; i++) {
            address token = _tokens[i];

            // 1. Get the total amount vested for this token.
            uint256 tokenAmount = vestedTotal[token];
            amounts[i] = tokenAmount;

            // 2. Query the oracle for this token’s USD price.
            //    Assumes the oracle returns a price scaled by 1e18.
            uint256 price = iPriceOracle(priceOracles[token]).getLatestPrice(token);

            // 3. Calculate the vested USD value: (price * amount) / 1e18
            uint256 valueInUsd = (price * tokenAmount) / 1e18;
            usdValues[i] = valueInUsd;

            // 4. Accumulate the total USD amount
            totalUsd += valueInUsd;
        }

        return (amounts, usdValues, totalUsd);
    }

    /// @notice Returns the total USD value of the user's unclaimed, uncomplete, stake amounts, based on current token prices from the oracle.
    /// @return totalUsd The total unclaimed stake value, in USD (1e18 precision).
    function getUserTotalUnclaimedUsdValue(address user) external view returns (uint256 totalUsd) {
        uint256 length = vestings[user].length;
        for (uint256 i = 0; i < length; i++) {
            Vesting memory v = vestings[user][i];
            if (!v.complete) {
                uint256 tokenPrice = iPriceOracle(priceOracles[v.token]).getLatestPrice(v.token);

                // The unclaimed portion of the stake
                uint256 unclaimedAmount = v.amount - v.claimedAmount;

                // Convert unclaimed tokens to USD value
                uint256 stakeUsd = (tokenPrice * unclaimedAmount) / 1e18;

                totalUsd += stakeUsd;
            }
        }
        return totalUsd;
    }

    
    /// @notice Function that lets you look up an address’s stake by stakeId.
    /// @param user The address to evaluate.
    /// @param _stakeId The stakeId of the ORIGINAL stake that is waiting to be unlocked
    function getWithdrawStake(address user, uint256 _stakeId) external view returns (WithdrawStake memory) {
        WithdrawStake[] storage userStakes = withdrawStake[user];
        for (uint256 i = 0; i < userStakes.length; i++) {
            if (userStakes[i].stakeId == _stakeId) {
                return userStakes[i];
            }
        }
        revert("WithdrawStake with the specified stakeId not found for this user.");
    }

    /// @notice Function that lets you look up an address’s stake by vestingId.
    /// @param user The address to evaluate.
    /// @param _vestingId The vestingId of the ORIGINAL vest that is waiting to be unlocked
    function getVestingWithdrawStake(address user, uint256 _vestingId) external view returns (WithdrawStake memory) {
        WithdrawStake[] storage userStakes = withdrawStake[user];
        uint256 boostedVestingId = _vestingId + 1e6;
        for (uint256 i = 0; i < userStakes.length; i++) {
            if (userStakes[i].stakeId == boostedVestingId) {
                return userStakes[i];
            }
        }
        revert("WithdrawStake with the specified stakeId not found for this user.");
    }

    /// @notice Function that returns an array of all the user's withdrawStakes.
    /// @param user The address to evaluate.
    /// @return An array of WithdrawStake for the given user.
    function getAllWithdrawStakes(address user) external view returns (WithdrawStake[] memory) {
        return withdrawStake[user];
    }

    /// @notice Function to put a stake for sale.
    ///         Sets the original stake amount to 0 to prevent any alterations while for sale.
    /// @param _stakeId The stake to sell.
    /// @param price The price of the stake.
    function sellStake(uint256 _stakeId, uint256 price) external {
        Stake storage stake = stakes[msg.sender][_stakeId];
        require(!stake.complete, "Stake already complete");
        require(stake.amount != 0, "Stake amount is 0");
        // Ensure the stake isn't already on sale.
        require(sellStakes[msg.sender][_stakeId].amount == 0, "Stake already on sale");

        // Create a SellStake entry directly in the mapping.
        sellStakes[msg.sender][_stakeId] = SellStake({
            price: price,
            bonusAmount: (price * sellKickBack) / 100,
            amount: stake.amount,
            lastClaimed: stake.lastClaimed,
            dailyRewardRate: stake.dailyRewardRate
        });

        // Lock the original stake by setting its amount to 0.
        stake.amount = 0;

        emit StakeUpForSale(msg.sender, price, _stakeId);
    }

    /// @notice Function to cancel a sell stake.
    ///         Restores the stake amount to the original stake and removes the sell stake.
    /// @param _stakeId The stake ID to cancel the sale.
    function cancelSellStake(uint256 _stakeId) external {
        SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId];
        require(sellStakeEntry.amount != 0, "Sell stake not found");

        // Access the original stake.
        Stake storage stake = stakes[msg.sender][_stakeId];
        require(stake.amount == 0, "Stake not in sell state");

        // Restore the original stake's amount.
        stake.amount = sellStakeEntry.amount;

        delete sellStakes[msg.sender][_stakeId];

        emit StakeSaleCancelled(msg.sender, _stakeId);
    }

    /// @notice Function to update the price of a stake that is for sale.
    /// @param _stakeId The stake ID to update.
    /// @param newPrice The new price of the stake.
    function updateSellStake(uint256 _stakeId, uint256 newPrice) external {
        SellStake storage sellStakeEntry = sellStakes[msg.sender][_stakeId];
        require(sellStakeEntry.amount != 0, "Sell stake not found");

        sellStakeEntry.bonusAmount = (newPrice * sellKickBack) / 100;
        sellStakeEntry.price = newPrice;

        emit StakeUpForSale(msg.sender, newPrice, _stakeId);
    }

    /// @notice Buys a sell stake.
    ///         Transfers the sale price from the buyer (using safeTransferFrom),
    ///         pays the seller (applying the sellTax),
    ///         creates a new stake for the buyer (amount = original amount + bonus),
    ///         marks the original stake as complete,
    ///         and sets the new stake's unlock time.
    /// @param seller The address of the seller.
    /// @param _stakeId The original stake id associated with the sell stake.
    function buySellStake(address seller, uint256 _stakeId) external {
        SellStake storage sellStakeEntry = sellStakes[seller][_stakeId];
        require(sellStakeEntry.amount != 0, "Sell stake not available");
        
        // Transfer the sale price from the buyer to this contract.
        IERC20(pool.tokenAddress).safeTransferFrom(msg.sender, address(this), sellStakeEntry.price);
        
        // Calculate the seller's payment using the sell tax.
        uint256 sellerPayment = (sellStakeEntry.price * sellTax) / 100;
        IERC20(pool.tokenAddress).safeTransfer(seller, sellerPayment);
        
        // Mark the original stake as complete.
        Stake storage originalStake = stakes[seller][_stakeId];
        originalStake.complete = true;
        
        // Create the new stake for the buyer using the inline push pattern.
        stakes[msg.sender].push(Stake({
            amount: sellStakeEntry.amount + sellStakeEntry.bonusAmount,
            lastClaimed: sellStakeEntry.lastClaimed,
            dailyRewardRate: sellStakeEntry.dailyRewardRate,
            unlockTime: block.timestamp + pool.lockupPeriod,
            complete: false
        }));
        
        // Remove the sell stake listing.
        delete sellStakes[seller][_stakeId];

        emit StakeSold(seller, msg.sender, sellStakeEntry.price, _stakeId);
    }
}

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

Context size (optional):