// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.7;
interface IERC721 {
function balanceOf(address owner) external view returns (uint256 balance);
function transferFrom(address from, address to, uint256 tokenId) external;
}
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(
address recipient,
uint256 amount
) external returns (bool);
}
contract StakeNFT {
uint256 private constant MINIMUM_NFT_BALANCE = 10;
uint256 private constant MAX_STAKE_AMOUNT = 500;
uint256 private constant MAX_STAKE_AMOUNT_PER_USER = 20;
address private constant NFTToken =
0x0FCBD68251819928C8f6D182fC04bE733fA94170; // LO-FI PEPE Collection
address private constant REWARDToken =
0x6982508145454Ce325dDbE47a25d4ec3d2311933; // PEPE token
// State variables
// 20 + 4 + 4 + 1 = 29 byte
address public admin;
uint32 private totalStaked;
uint32 private stakingId;
bool private started;
uint256 private constant rate = 5_200_000e18; // per staking for 90 days
uint256 private constant stakingPeriod = 90 days;
uint private immutable startTimestamp; // 1735156800 = Wed Dec 25 2024 20:00:00 GMT+0000
uint private immutable endTimestamp;
// Structs
struct Staking {
// 20 + 5 + 1 + 4 = 30 byte
address staker;
uint40 releaseTime;
bool isCanceled;
uint32 stakingId;
// 4 + 16 + 5 = 25 byte
uint32 tokenId;
uint128 claimedAmount;
uint40 stakeTime;
}
// Mapping
mapping(address => uint256) private stakedCount;
mapping(uint32 => Staking) private _stakedItem;
mapping(address => bool) private allowList;
// Errors
error Paused();
error InvalidNFTContract();
error InvalidLength();
error NotAvailableToStake();
error MaxStakeAmountPerUserReached();
error MaxStakeAmountReached();
error NotEnoughNFTs();
error StakingPeriodIsNotOver();
error NotStaker();
error NotActiveStatus();
error NotAdmin();
// Events
event TokenStaked(
address indexed staker,
uint256 tokenId,
bool isStaked,
uint256 stakingId
);
event TokenClaimStatus(
uint256 indexed tokenId,
bool isActive,
uint256 stakingId
);
event TokenClaimComplete(
uint256 indexed tokenId,
bool isActive,
uint256 stakingId
);
event TokenCancelComplete(
uint256 indexed tokenId,
bool isCancelled,
uint256 stakingId
);
// Constructor
constructor() {
startTimestamp = 1735156800; // Wed Dec 25 2024 20:00:00 GMT+0000
endTimestamp = startTimestamp + stakingPeriod;
admin = msg.sender;
}
modifier onlyAdmin() {
if (admin != msg.sender) revert NotAdmin();
_;
}
// Function to call another function
function callStakeToken(
address _token,
uint256[] memory _tokenIDs
) external {
if (started) revert Paused();
if (_token != NFTToken) revert InvalidNFTContract();
if (_tokenIDs.length == 0) revert InvalidLength();
// Check if staking period not started or is over.
if (block.timestamp > endTimestamp || block.timestamp < startTimestamp)
revert NotAvailableToStake();
uint256 userStakedCount = stakedCount[msg.sender];
// Check if MAX_STAKE_AMOUNT_PER_USER is exceeded.
if (userStakedCount + _tokenIDs.length > MAX_STAKE_AMOUNT_PER_USER)
revert MaxStakeAmountPerUserReached();
// Check if msg.sender is allowed to have fewer than 10 NFTs to stake.
if (!allowList[msg.sender]) {
// Staker should have at least 10 NFT.
uint256 balance = IERC721(NFTToken).balanceOf(msg.sender);
if (balance + userStakedCount < MINIMUM_NFT_BALANCE)
revert NotEnoughNFTs();
}
_stakeToken(_tokenIDs);
}
// Function to transfer NFT from user to contract
function _stakeToken(uint256[] memory _tokenIds) internal {
uint256 tokenIdLength = _tokenIds.length;
uint32 currentTotalStaked = totalStaked;
// Check if maximum stake amount is reached.
if (currentTotalStaked + tokenIdLength > MAX_STAKE_AMOUNT)
revert MaxStakeAmountReached();
uint32 currentStakingId = stakingId;
for (uint256 i; i < tokenIdLength; ) {
IERC721(NFTToken).transferFrom(
msg.sender,
address(this),
_tokenIds[i]
);
Staking memory staking = Staking(
msg.sender, // staker
uint40(block.timestamp), // releaseTime
false, // isCancelled
currentStakingId, // stakingId
uint32(_tokenIds[i]), // tokenId
0, // claimedAmount
uint40(block.timestamp) // stakeTime
);
_stakedItem[currentStakingId] = staking;
emit TokenStaked(msg.sender, _tokenIds[i], true, currentStakingId);
unchecked {
currentStakingId++;
currentTotalStaked++;
i++;
}
}
stakingId = currentStakingId;
totalStaked = currentTotalStaked;
stakedCount[msg.sender] += tokenIdLength;
}
// Function to view staked NFT
function viewStake(
uint32 _stakingId
) external view returns (Staking memory) {
return _stakedItem[_stakingId];
}
// Function to get list of staked tokens
function viewMyStakedNfts(
address user
) external view returns (Staking[] memory) {
Staking[] memory list = new Staking[](stakedCount[user]);
uint256 cnt;
for (uint32 index; index < stakingId; index++) {
if (
_stakedItem[index].staker == user &&
_stakedItem[index].isCanceled == false
) {
list[cnt++] = _stakedItem[index];
}
}
return list;
}
// Function to check NFT stake duration status
function checkStake(
uint32 _stakingId,
address _staker
) external returns (Staking memory) {
Staking memory staking = _stakedItem[_stakingId];
if (staking.staker != _staker) revert NotStaker();
if (staking.isCanceled) revert NotActiveStatus();
emit TokenClaimStatus(staking.tokenId, true, _stakingId);
return staking;
}
// Function to claim reward token if NFT stake duration is completed
function claimReward(uint32 _stakingId) public {
Staking storage staking = _stakedItem[_stakingId];
if (staking.isCanceled) revert NotActiveStatus();
uint256 maxTimestamp = endTimestamp;
uint256 curTimestamp = block.timestamp;
uint256 releaseTime = curTimestamp < maxTimestamp
? curTimestamp
: maxTimestamp;
if (releaseTime <= staking.releaseTime) return;
uint256 amount = (rate / stakingPeriod) *
(releaseTime - staking.releaseTime);
uint256 balance = IERC20(REWARDToken).balanceOf(address(this));
if (balance < amount) {
amount = balance;
}
staking.releaseTime = uint40(releaseTime);
staking.claimedAmount += uint128(amount);
emit TokenClaimComplete(staking.tokenId, true, _stakingId);
if (amount != 0) {
IERC20(REWARDToken).transfer(staking.staker, amount);
}
}
function unStakeAll(uint32[] memory _stakingIds) external onlyAdmin {
if (block.timestamp < endTimestamp) revert StakingPeriodIsNotOver();
uint256 length = _stakingIds.length;
for (uint256 i; i < length; ) {
Staking storage staking = _stakedItem[_stakingIds[i]];
claimReward(_stakingIds[i]);
staking.isCanceled = true;
address recipient = staking.staker;
// Return NFT back to user.
IERC721(NFTToken).transferFrom(
address(this),
recipient,
staking.tokenId
);
unchecked {
stakedCount[recipient]--;
i++;
}
}
totalStaked -= uint32(length);
}
// Function to cancel NFT stake
function unStake(uint32[] memory _stakingIds) external {
uint256 length = _stakingIds.length;
if (length == 0) revert InvalidLength();
for (uint256 i; i < length; ) {
Staking storage staking = _stakedItem[_stakingIds[i]];
if (staking.staker != msg.sender) revert NotStaker();
// Claim the reward.
claimReward(_stakingIds[i]);
staking.isCanceled = true;
uint256 tokenId = staking.tokenId;
// Transfer tokenId to msg.sender. Here msg.sender == staking.staker
IERC721(NFTToken).transferFrom(address(this), msg.sender, tokenId);
emit TokenCancelComplete(tokenId, true, _stakingIds[i]);
unchecked {
i++;
}
}
totalStaked -= uint32(length);
stakedCount[msg.sender] -= length;
}
function updateAllowList(
address[] memory _addresses,
bool _isAllowed
) external onlyAdmin {
uint256 length = _addresses.length;
for (uint256 i; i < length; i++) {
allowList[_addresses[i]] = _isAllowed;
}
}
function withdraw(uint256 amount) external onlyAdmin {
IERC20(REWARDToken).transfer(msg.sender, amount);
}
function toggleStake(bool newStat) external onlyAdmin {
started = newStat;
}
function setNewAdmin(address newAdd) external onlyAdmin {
admin = newAdd;
}
function getRewardRate() external pure returns (uint256) {
return rate;
}
function getNFTAddress() external pure returns (address) {
return NFTToken;
}
function getRewardToken() external pure returns (address) {
return REWARDToken;
}
function getStakingIndex() external view returns (uint256) {
return stakingId;
}
function getTotalStaked() external view returns (uint256) {
return totalStaked;
}
}
{
"compilationTarget": {
"StakeNFT.sol": "StakeNFT"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"InvalidLength","type":"error"},{"inputs":[],"name":"InvalidNFTContract","type":"error"},{"inputs":[],"name":"MaxStakeAmountPerUserReached","type":"error"},{"inputs":[],"name":"MaxStakeAmountReached","type":"error"},{"inputs":[],"name":"NotActiveStatus","type":"error"},{"inputs":[],"name":"NotAdmin","type":"error"},{"inputs":[],"name":"NotAvailableToStake","type":"error"},{"inputs":[],"name":"NotEnoughNFTs","type":"error"},{"inputs":[],"name":"NotStaker","type":"error"},{"inputs":[],"name":"Paused","type":"error"},{"inputs":[],"name":"StakingPeriodIsNotOver","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bool","name":"isCancelled","type":"bool"},{"indexed":false,"internalType":"uint256","name":"stakingId","type":"uint256"}],"name":"TokenCancelComplete","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bool","name":"isActive","type":"bool"},{"indexed":false,"internalType":"uint256","name":"stakingId","type":"uint256"}],"name":"TokenClaimComplete","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bool","name":"isActive","type":"bool"},{"indexed":false,"internalType":"uint256","name":"stakingId","type":"uint256"}],"name":"TokenClaimStatus","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bool","name":"isStaked","type":"bool"},{"indexed":false,"internalType":"uint256","name":"stakingId","type":"uint256"}],"name":"TokenStaked","type":"event"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256[]","name":"_tokenIDs","type":"uint256[]"}],"name":"callStakeToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_stakingId","type":"uint32"},{"internalType":"address","name":"_staker","type":"address"}],"name":"checkStake","outputs":[{"components":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint40","name":"releaseTime","type":"uint40"},{"internalType":"bool","name":"isCanceled","type":"bool"},{"internalType":"uint32","name":"stakingId","type":"uint32"},{"internalType":"uint32","name":"tokenId","type":"uint32"},{"internalType":"uint128","name":"claimedAmount","type":"uint128"},{"internalType":"uint40","name":"stakeTime","type":"uint40"}],"internalType":"struct StakeNFT.Staking","name":"","type":"tuple"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"_stakingId","type":"uint32"}],"name":"claimReward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getNFTAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getRewardRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getRewardToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getStakingIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalStaked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newAdd","type":"address"}],"name":"setNewAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"newStat","type":"bool"}],"name":"toggleStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32[]","name":"_stakingIds","type":"uint32[]"}],"name":"unStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32[]","name":"_stakingIds","type":"uint32[]"}],"name":"unStakeAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_addresses","type":"address[]"},{"internalType":"bool","name":"_isAllowed","type":"bool"}],"name":"updateAllowList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"viewMyStakedNfts","outputs":[{"components":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint40","name":"releaseTime","type":"uint40"},{"internalType":"bool","name":"isCanceled","type":"bool"},{"internalType":"uint32","name":"stakingId","type":"uint32"},{"internalType":"uint32","name":"tokenId","type":"uint32"},{"internalType":"uint128","name":"claimedAmount","type":"uint128"},{"internalType":"uint40","name":"stakeTime","type":"uint40"}],"internalType":"struct StakeNFT.Staking[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"_stakingId","type":"uint32"}],"name":"viewStake","outputs":[{"components":[{"internalType":"address","name":"staker","type":"address"},{"internalType":"uint40","name":"releaseTime","type":"uint40"},{"internalType":"bool","name":"isCanceled","type":"bool"},{"internalType":"uint32","name":"stakingId","type":"uint32"},{"internalType":"uint32","name":"tokenId","type":"uint32"},{"internalType":"uint128","name":"claimedAmount","type":"uint128"},{"internalType":"uint40","name":"stakeTime","type":"uint40"}],"internalType":"struct StakeNFT.Staking","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]