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);
}
}