Contract Name:
OOGIESstaking
Contract Source Code:
File 1 of 1 : OOGIESstaking
// File: @openzeppelin/contracts/utils/introspection/IERC165.sol
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* 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[EIP 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);
}
// File: @openzeppelin/contracts/token/ERC721/IERC721.sol
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.20;
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or
* {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// File: @openzeppelin/contracts/token/ERC20/IERC20.sol
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
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);
}
// File: @openzeppelin/contracts/security/ReentrancyGuard.sol
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
// File: @openzeppelin/contracts/utils/Context.sol
// OpenZeppelin Contracts (last updated v5.0.0) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
// File: @openzeppelin/contracts/access/Ownable.sol
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// File: contracts/OOGIESstaking.sol
pragma solidity ^0.8.17;
/**
* @title NFTStaking
* @notice Stake ERC721 tokens to earn ERC20 rewards (AFTR) from this contract's balance,
* with optional locks of 30/60/90 days at higher multipliers (1.5x, 2x, 2.5x).
*/
contract OOGIESstaking is ReentrancyGuard, Ownable {
// ----------------------------------------
// State Variables
// ----------------------------------------
/// @notice The ERC721 contract whose NFTs we are staking.
IERC721 public nftContract = IERC721(0x214cAE51c3BAE88515aAEfd8e1867E64502B0342);
/// @notice The ERC20 token used as the reward (must be deposited into this contract).
IERC20 public rewardToken = IERC20(0xA0a3b1Fdb070B4b99E76E23D983F3000F093A102);
/// @notice Number of seconds in 1 day (used for integer division).
uint256 public dayInSeconds = 1 days;
/**
* @notice Base daily reward rate (in wei).
* e.g. 1 ether = 1 token/day if token has 18 decimals.
*/
uint256 public rewardRateNormal = 5 ether;
/**
* @notice Lock durations and multipliers (all editable by admin).
* Example multipliers: 150 = 1.5x, 200 = 2x, 250 = 2.5x.
*/
uint256 public lockDuration30 = 30 days;
uint256 public lockMultiplier30 = 150; // 1.5x
uint256 public lockDuration60 = 60 days;
uint256 public lockMultiplier60 = 200; // 2.0x
uint256 public lockDuration90 = 90 days;
uint256 public lockMultiplier90 = 250; // 2.5x
/**
* @notice NFT-specific multipliers: e.g., 150 = 1.5x, 200 = 2x.
* If 0, treat it as 100 (1x).
*/
mapping(uint256 => uint256) public nftMultiplier;
/// @notice Lock option choices
enum LockOption {
None, // no lock (normal staking)
Lock30, // 30-day lock at lockMultiplier30
Lock60, // 60-day lock at lockMultiplier60
Lock90 // 90-day lock at lockMultiplier90
}
/// @notice Info about each staked NFT.
struct StakedNFT {
address owner; // who staked it
uint256 tokenId; // which token
uint256 stakedAt; // when it was staked
uint256 lastClaimedAt; // last time rewards were claimed
LockOption lockOption; // which lock type
uint256 lockStartedAt; // when current lock was set
}
/// @notice tokenId => staking info
mapping(uint256 => StakedNFT) public stakedNFTs;
/// @notice Keep track of all currently staked tokenIds (for enumerations).
uint256[] private allStakedTokenIds;
/// @notice tokenId => index in allStakedTokenIds
mapping(uint256 => uint256) private allStakedTokenIndex;
/// @notice For easy lookup of which tokenIds a user has staked.
mapping(address => uint256[]) private userStakedTokens;
/// @notice tokenId => index in userStakedTokens[user]
mapping(uint256 => uint256) private userStakedTokensIndex;
// ----------------------------------------
// Events
// ----------------------------------------
event Staked(address indexed user, uint256 tokenId, LockOption lockOption);
event Unstaked(address indexed user, uint256 tokenId);
event RewardClaimed(address indexed user, uint256 tokenId, uint256 amount);
event LockOptionChanged(address indexed user, uint256 tokenId, LockOption newLockOption);
event TokensDeposited(address indexed from, uint256 amount);
// ----------------------------------------
// Constructor
// ----------------------------------------
constructor(address _initialOwner) Ownable(_initialOwner) {}
// ----------------------------------------
// Owner-Only Functions
// ----------------------------------------
function setNftContract(IERC721 _nftContract) external onlyOwner {
nftContract = _nftContract;
}
function setRewardToken(IERC20 _rewardToken) external onlyOwner {
rewardToken = _rewardToken;
}
function setDayInSeconds(uint256 _seconds) external onlyOwner {
dayInSeconds = _seconds;
}
function setRewardRateNormal(uint256 _normal) external onlyOwner {
rewardRateNormal = _normal;
}
/**
* @notice Update lock durations and/or multipliers for 30, 60, and 90 days.
* Example multipliers: 150 => 1.5x, 200 => 2x, 250 => 2.5x.
*/
function setLockParams(
uint256 _lockDuration30,
uint256 _lockMultiplier30,
uint256 _lockDuration60,
uint256 _lockMultiplier60,
uint256 _lockDuration90,
uint256 _lockMultiplier90
) external onlyOwner {
lockDuration30 = _lockDuration30;
lockMultiplier30 = _lockMultiplier30;
lockDuration60 = _lockDuration60;
lockMultiplier60 = _lockMultiplier60;
lockDuration90 = _lockDuration90;
lockMultiplier90 = _lockMultiplier90;
}
/**
* @notice Set multipliers for multiple NFTs at once.
* @param _tokenIds array of token IDs
* @param _multipliers array of multipliers (e.g. 150 = 1.5x), must match length of _tokenIds
*/
function setNFTMultiplierBatch(uint256[] calldata _tokenIds, uint256[] calldata _multipliers) external onlyOwner {
require(_tokenIds.length == _multipliers.length, "Array length mismatch");
for (uint256 i = 0; i < _tokenIds.length; i++) {
nftMultiplier[_tokenIds[i]] = _multipliers[i];
}
}
// ----------------------------------
// Rescue functions
// ----------------------------------
function rescueNative(uint256 _amount) external onlyOwner {
require(address(this).balance >= _amount, "Not enough native balance");
payable(msg.sender).transfer(_amount);
}
function rescueERC20(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).transfer(msg.sender, _amount);
}
// ----------------------------------------
// Deposit Rewards (ERC20)
// ----------------------------------------
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Cannot deposit 0");
bool success = rewardToken.transferFrom(msg.sender, address(this), amount);
require(success, "transferFrom failed");
emit TokensDeposited(msg.sender, amount);
}
function getContractBalance() external view returns (uint256) {
return rewardToken.balanceOf(address(this));
}
// ----------------------------------------
// Batch Operations
// ----------------------------------------
/**
* @notice Stake multiple NFTs at once with a chosen lock option (None, Lock30, Lock60, Lock90).
* @param _tokenIds array of token IDs
* @param _lockOptions array of lock options
*/
function stakeBatch(uint256[] calldata _tokenIds, LockOption[] calldata _lockOptions) external nonReentrant {
require(_tokenIds.length == _lockOptions.length, "Array length mismatch");
for (uint256 i = 0; i < _tokenIds.length; i++) {
_stakeInternal(_tokenIds[i], _lockOptions[i]);
}
}
/**
* @notice Unstake multiple NFTs at once.
* Reverts if the lock period has not ended yet.
*/
function unstakeBatch(uint256[] calldata _tokenIds) external nonReentrant {
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
require(stakedNFTs[tokenId].owner == msg.sender, "Not staker");
_unstakeInternal(tokenId);
}
}
/**
* @notice Claim rewards for multiple NFTs at once.
*/
function claimRewardsBatch(uint256[] calldata _tokenIds) external nonReentrant {
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
require(stakedNFTs[tokenId].owner == msg.sender, "Not staker");
_claimRewards(tokenId);
}
}
/**
* @notice Lock multiple NFTs that are currently staked but unlocked (None).
* Or if they were locked and the lock is finished, this also acts as “re-lock.”
* @dev If the lock is still active and not finished, this will revert.
*/
function lockStakedNFTBatch(uint256[] calldata _tokenIds, LockOption[] calldata _lockOptions)
external
nonReentrant
{
require(_tokenIds.length == _lockOptions.length, "Array length mismatch");
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
require(stakedNFTs[tokenId].owner == msg.sender, "Not staker");
_lockStakedNFTInternal(tokenId, _lockOptions[i]);
}
}
/**
* @notice Re-lock multiple NFTs (choose any lock option) once your previous lock ended.
* @dev If lock not ended, will revert; if not locked at all, will revert.
*/
function reLockBatch(uint256[] calldata _tokenIds, LockOption[] calldata _newLockOptions) external nonReentrant {
require(_tokenIds.length == _newLockOptions.length, "Array length mismatch");
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
require(stakedNFTs[tokenId].owner == msg.sender, "Not staker");
_reLockInternal(tokenId, _newLockOptions[i]);
}
}
// ----------------------------------------
// Batch Views (Claimable Rewards, etc.)
// ----------------------------------------
/**
* @notice Returns an array of claimable rewards for the given NFT IDs, in the same order.
*/
function claimableRewardsBatch(uint256[] calldata _tokenIds) external view returns (uint256[] memory) {
uint256[] memory amounts = new uint256[](_tokenIds.length);
for (uint256 i = 0; i < _tokenIds.length; i++) {
amounts[i] = _claimableRewardsView(_tokenIds[i]);
}
return amounts;
}
/**
* @notice Returns how many tokens (AFTR) this user generates per full day
* across all staked NFTs, factoring in lock options and NFT multipliers.
*/
function generatingPerDay(address _user) external view returns (uint256) {
uint256 totalDailyRate = 0;
uint256[] memory tokenIds = userStakedTokens[_user];
for (uint256 i = 0; i < tokenIds.length; i++) {
uint256 tokenId = tokenIds[i];
StakedNFT storage info = stakedNFTs[tokenId];
// sanity check
if (info.owner != _user) continue;
// figure out if still in lock period
(uint256 chosenLockMult, bool inLock) = _getLockMultiplierAndStatus(info);
uint256 baseDaily;
if (inLock) {
// still in lock => base rate * chosenLockMult
baseDaily = (rewardRateNormal * chosenLockMult) / 100;
} else {
// not locked or lock ended => normal rate
baseDaily = rewardRateNormal;
}
// apply NFT-specific multiplier
uint256 nftMult = nftMultiplier[tokenId];
if (nftMult == 0) nftMult = 100;
uint256 finalRate = (baseDaily * nftMult) / 100;
totalDailyRate += finalRate;
}
return totalDailyRate;
}
// ----------------------------------------
// Internal (Single-Token) Logic
// ----------------------------------------
// Called by the batch functions above.
function _stakeInternal(uint256 _tokenId, LockOption _lockOption) internal {
// Transfer NFT from user to this contract
nftContract.transferFrom(msg.sender, address(this), _tokenId);
// Record staking info
StakedNFT storage info = stakedNFTs[_tokenId];
info.owner = msg.sender;
info.tokenId = _tokenId;
info.stakedAt = block.timestamp;
info.lastClaimedAt = block.timestamp;
info.lockOption = _lockOption;
info.lockStartedAt = (_lockOption == LockOption.None) ? 0 : block.timestamp;
// Add to global and user tracking
_addToAllStakedTokenIds(_tokenId);
_addToUserStakedTokens(msg.sender, _tokenId);
emit Staked(msg.sender, _tokenId, _lockOption);
}
function _unstakeInternal(uint256 _tokenId) internal {
StakedNFT storage info = stakedNFTs[_tokenId];
// if locked, ensure lock period ended
if (info.lockOption != LockOption.None) {
uint256 lockEnd = info.lockStartedAt + _getLockDuration(info.lockOption);
require(block.timestamp >= lockEnd, "Lock period not over. Cannot unstake yet.");
}
// claim all rewards up to now
_claimRewards(_tokenId);
// transfer NFT back
nftContract.transferFrom(address(this), info.owner, _tokenId);
// cleanup
_removeFromAllStakedTokenIds(_tokenId);
_removeFromUserStakedTokens(info.owner, _tokenId);
delete stakedNFTs[_tokenId];
emit Unstaked(info.owner, _tokenId);
}
/**
* @dev Lock an NFT that is currently staked but:
* - either has never been locked (lockOption == None), or
* - was locked and its lock period is finished (so effectively “re-lock”).
* If it's still locked and not finished, this reverts.
*/
function _lockStakedNFTInternal(uint256 _tokenId, LockOption _newLockOption) internal {
StakedNFT storage info = stakedNFTs[_tokenId];
// Must be staked
require(info.owner != address(0), "Not staked");
// If it's currently locked, check if the lock ended
if (info.lockOption != LockOption.None) {
uint256 lockEnd = info.lockStartedAt + _getLockDuration(info.lockOption);
require(block.timestamp >= lockEnd, "Current lock not finished");
}
// First claim everything earned so far (including any unlocked days)
_claimRewards(_tokenId);
// Set new lock
info.lockOption = _newLockOption;
info.lockStartedAt = block.timestamp;
info.lastClaimedAt = block.timestamp; // so we start fresh
emit LockOptionChanged(info.owner, _tokenId, _newLockOption);
}
/**
* @dev Re-lock an NFT that was already locked, picking a new lock option
* after the old lock has ended. Reverts if lock not over or was never locked.
*/
function _reLockInternal(uint256 _tokenId, LockOption _newLockOption) internal {
StakedNFT storage info = stakedNFTs[_tokenId];
// Must already be locked with some option
require(info.lockOption != LockOption.None, "NFT not currently locked");
// Ensure old lock ended
uint256 lockEnd = info.lockStartedAt + _getLockDuration(info.lockOption);
require(block.timestamp >= lockEnd, "Current lock not finished");
// Claim all rewards up to now
_claimRewards(_tokenId);
// Re-lock with the new option
info.lockOption = _newLockOption;
info.lockStartedAt = block.timestamp;
info.lastClaimedAt = block.timestamp;
emit LockOptionChanged(info.owner, _tokenId, _newLockOption);
}
// ----------------------------------------
// Reward Logic
// ----------------------------------------
/**
* @dev Internal function to calculate and transfer rewards from the contract’s balance.
*/
function _claimRewards(uint256 _tokenId) internal returns (uint256) {
StakedNFT storage info = stakedNFTs[_tokenId];
uint256 currentTime = block.timestamp;
uint256 lastClaimed = info.lastClaimedAt;
// no time passed => no rewards
if (currentTime <= lastClaimed) {
return 0;
}
uint256 timeDiff = currentTime - lastClaimed;
uint256 totalReward = 0;
// figure out lock end
uint256 lockEnd = (info.lockOption == LockOption.None)
? 0
: (info.lockStartedAt + _getLockDuration(info.lockOption));
// break the time into possible segments: locked vs. unlocked
if (info.lockOption != LockOption.None) {
// some lock option
if (lastClaimed < lockEnd) {
// portion in lock
uint256 lockedSegmentEnd = (currentTime < lockEnd) ? currentTime : lockEnd;
uint256 lockedTime = (lockedSegmentEnd > lastClaimed)
? lockedSegmentEnd - lastClaimed
: 0;
uint256 lockedDays = lockedTime / dayInSeconds;
uint256 lockedMult = _getLockMultiplier(info.lockOption);
// locked portion daily rate = rewardRateNormal * lockedMult/100
// we apply the NFT multiplier below
uint256 lockedDailyRate = (rewardRateNormal * lockedMult) / 100;
// NFT-specific multiplier
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
// final locked portion = lockedDays * lockedDailyRate * (nftMult/100)
uint256 lockedReward = lockedDays * ((lockedDailyRate * nftMult) / 100);
totalReward += lockedReward;
// portion after lock (if currentTime > lockEnd)
if (currentTime > lockEnd) {
uint256 postLockTime = currentTime - lockEnd;
uint256 postLockDays = postLockTime / dayInSeconds;
// post-lock daily rate => normal rate * NFT multiplier
uint256 postLockDailyRate = (rewardRateNormal * nftMult) / 100;
totalReward += postLockDays * postLockDailyRate;
}
} else {
// entire interval is after lock ended
uint256 daysCount = timeDiff / dayInSeconds;
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
uint256 postLockDaily = (rewardRateNormal * nftMult) / 100;
totalReward += daysCount * postLockDaily;
}
} else {
// no lock => purely normal
uint256 daysCount = timeDiff / dayInSeconds;
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
uint256 daily = (rewardRateNormal * nftMult) / 100;
totalReward += daysCount * daily;
}
// update lastClaimed
info.lastClaimedAt = currentTime;
// transfer from contract to staker
if (totalReward > 0) {
uint256 contractBal = rewardToken.balanceOf(address(this));
require(contractBal >= totalReward, "Not enough reward tokens in contract");
rewardToken.transfer(info.owner, totalReward);
emit RewardClaimed(info.owner, _tokenId, totalReward);
}
return totalReward;
}
/**
* @dev View-only version of claiming logic (no state changes).
*/
function _claimableRewardsView(uint256 _tokenId) internal view returns (uint256) {
StakedNFT storage info = stakedNFTs[_tokenId];
if (info.owner == address(0)) return 0; // not staked
uint256 currentTime = block.timestamp;
uint256 lastClaimed = info.lastClaimedAt;
if (currentTime <= lastClaimed) return 0;
uint256 timeDiff = currentTime - lastClaimed;
uint256 totalReward = 0;
uint256 lockEnd = (info.lockOption == LockOption.None)
? 0
: (info.lockStartedAt + _getLockDuration(info.lockOption));
if (info.lockOption != LockOption.None) {
if (lastClaimed < lockEnd) {
uint256 lockedSegmentEnd = (currentTime < lockEnd) ? currentTime : lockEnd;
uint256 lockedTime = (lockedSegmentEnd > lastClaimed)
? lockedSegmentEnd - lastClaimed
: 0;
uint256 lockedDays = lockedTime / dayInSeconds;
uint256 lockedMult = _getLockMultiplier(info.lockOption);
uint256 lockedDailyRate = (rewardRateNormal * lockedMult) / 100;
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
uint256 lockedReward = lockedDays * ((lockedDailyRate * nftMult) / 100);
totalReward += lockedReward;
if (currentTime > lockEnd) {
uint256 postLockTime = currentTime - lockEnd;
uint256 postLockDays = postLockTime / dayInSeconds;
uint256 postLockDaily = (rewardRateNormal * nftMult) / 100;
totalReward += postLockDays * postLockDaily;
}
} else {
// entire interval after lock
uint256 daysCount = timeDiff / dayInSeconds;
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
uint256 postLockDaily = (rewardRateNormal * nftMult) / 100;
totalReward += daysCount * postLockDaily;
}
} else {
// no lock
uint256 daysCount = timeDiff / dayInSeconds;
uint256 nftMult = nftMultiplier[_tokenId];
if (nftMult == 0) nftMult = 100;
uint256 daily = (rewardRateNormal * nftMult) / 100;
totalReward += daysCount * daily;
}
return totalReward;
}
// ----------------------------------------
// View Functions (Enumeration, etc.)
// ----------------------------------------
function totalStaked() external view returns (uint256) {
return allStakedTokenIds.length;
}
function getAllStakedNFTs() external view returns (StakedNFT[] memory) {
uint256 length = allStakedTokenIds.length;
StakedNFT[] memory result = new StakedNFT[](length);
for (uint256 i = 0; i < length; i++) {
uint256 tokenId = allStakedTokenIds[i];
result[i] = stakedNFTs[tokenId];
}
return result;
}
function getStakedNFTsOf(address _wallet) external view returns (StakedNFT[] memory) {
uint256[] storage tokenIds = userStakedTokens[_wallet];
uint256 length = tokenIds.length;
StakedNFT[] memory result = new StakedNFT[](length);
for (uint256 i = 0; i < length; i++) {
uint256 tokenId = tokenIds[i];
result[i] = stakedNFTs[tokenId];
}
return result;
}
// ----------------------------------------
// Internal Helpers
// ----------------------------------------
function _addToAllStakedTokenIds(uint256 _tokenId) internal {
allStakedTokenIndex[_tokenId] = allStakedTokenIds.length;
allStakedTokenIds.push(_tokenId);
}
function _removeFromAllStakedTokenIds(uint256 _tokenId) internal {
uint256 idx = allStakedTokenIndex[_tokenId];
uint256 lastIndex = allStakedTokenIds.length - 1;
if (idx != lastIndex) {
uint256 lastTokenId = allStakedTokenIds[lastIndex];
allStakedTokenIds[idx] = lastTokenId;
allStakedTokenIndex[lastTokenId] = idx;
}
allStakedTokenIds.pop();
delete allStakedTokenIndex[_tokenId];
}
function _addToUserStakedTokens(address _user, uint256 _tokenId) internal {
userStakedTokensIndex[_tokenId] = userStakedTokens[_user].length;
userStakedTokens[_user].push(_tokenId);
}
function _removeFromUserStakedTokens(address _user, uint256 _tokenId) internal {
uint256 idx = userStakedTokensIndex[_tokenId];
uint256 lastIndex = userStakedTokens[_user].length - 1;
if (idx != lastIndex) {
uint256 lastTokenId = userStakedTokens[_user][lastIndex];
userStakedTokens[_user][idx] = lastTokenId;
userStakedTokensIndex[lastTokenId] = idx;
}
userStakedTokens[_user].pop();
delete userStakedTokensIndex[_tokenId];
}
/**
* @dev Returns the configured lock duration (in seconds) for the chosen option.
*/
function _getLockDuration(LockOption option) internal view returns (uint256) {
if (option == LockOption.Lock30) {
return lockDuration30;
} else if (option == LockOption.Lock60) {
return lockDuration60;
} else if (option == LockOption.Lock90) {
return lockDuration90;
}
return 0;
}
/**
* @dev Returns the lock multiplier (e.g. 150 => 1.5×) for the chosen lock option.
*/
function _getLockMultiplier(LockOption option) internal view returns (uint256) {
if (option == LockOption.Lock30) {
return lockMultiplier30;
} else if (option == LockOption.Lock60) {
return lockMultiplier60;
} else if (option == LockOption.Lock90) {
return lockMultiplier90;
}
return 100; // None => normal
}
/**
* @dev Helper to see if a given staked NFT is still within its lock period,
* plus the chosen lock multiplier.
*/
function _getLockMultiplierAndStatus(
StakedNFT storage info
)
internal
view
returns (uint256 chosenMultiplier, bool stillInLock)
{
if (info.lockOption == LockOption.None) {
return (100, false);
}
uint256 duration = _getLockDuration(info.lockOption);
uint256 endTime = info.lockStartedAt + duration;
bool inLockPeriod = (block.timestamp < endTime);
uint256 lockMult = _getLockMultiplier(info.lockOption);
return (lockMult, inLockPeriod);
}
/**
* @notice Forcibly return an array of staked NFTs to their respective owners.
* No rewards are claimed or paid. Resets all internal tracking.
* @param _tokenIds Array of token IDs to be returned.
*/
function emergencyReturnBatch(uint256[] calldata _tokenIds) external onlyOwner nonReentrant {
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
StakedNFT storage info = stakedNFTs[tokenId];
// If it's actually staked (owner != address(0))
if (info.owner != address(0)) {
// Transfer NFT back to staker
nftContract.transferFrom(address(this), info.owner, tokenId);
// Remove from global array
_removeFromAllStakedTokenIds(tokenId);
// Remove from user array
_removeFromUserStakedTokens(info.owner, tokenId);
// Clear staking info (no rewards)
delete stakedNFTs[tokenId];
}
}
}
/**
* @notice Forcibly return ALL currently staked NFTs to their respective owners.
* No rewards are claimed or paid. Resets all internal tracking for each NFT.
*/
function emergencyReturnAllStaked() external onlyOwner nonReentrant {
// Keep removing from the end of allStakedTokenIds until empty
while (allStakedTokenIds.length > 0) {
uint256 lastIndex = allStakedTokenIds.length - 1;
uint256 tokenId = allStakedTokenIds[lastIndex];
StakedNFT storage info = stakedNFTs[tokenId];
if (info.owner != address(0)) {
// Transfer NFT back to staker
nftContract.transferFrom(address(this), info.owner, tokenId);
// Remove from user array
_removeFromUserStakedTokens(info.owner, tokenId);
// Clear staking info
delete stakedNFTs[tokenId];
}
// Finally remove it from the global array
allStakedTokenIds.pop();
}
}
/**
* @notice Allow contract owner to withdraw any NFT directly to their own wallet,
* ignoring normal staking rules. Use with care!
* @param _tokenId Token ID to withdraw to the owner's wallet.
*/
function emergencyWithdrawNFT(uint256 _tokenId) external onlyOwner nonReentrant {
nftContract.transferFrom(address(this), owner(), _tokenId);
}
}