π©Staking Contract
In this page we go more deeply in the process of stacking for the most techie people.
// // SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
interface INftCollection is IERC721 {
function mintLemur(address to) external;
function mintRhino(address to) external;
function mintGorilla(address to) external;
function getIndexForId(uint256 _id) external pure returns (uint256);
function getFeeForId(uint256 _id) external view returns (uint256);
function getExtraAmount(uint256 _id) external view returns (uint256);
}
interface IERC20USDC {
/**
* @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 amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` 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 amount) 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 `amount` 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 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` 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 amount
) external returns (bool);
}
interface ISingleSwap {
function SellingUSDCToken(
uint256 amountIn
) external returns (uint256 amountOut);
}
interface IZooToken {
function allowance(
address owner,
address spender
) external view returns (uint256);
function approve(address spender, uint256 amount) external;
function mint(address to, uint256 amount) external;
function transferFrom(address from, address to, uint256 value) external;
function transfer(address to, uint256 amount) external;
function balanceOf(address account) external view returns (uint256);
function totalSupply() external view returns (uint256);
}
contract CrazyZooStaking {
address public _USDCToken;
INftCollection public nftCollection;
ISingleSwap public swap;
IZooToken public ZooToken;
IERC20USDC public USDCToken;
//staking storage
struct StakedNft {
uint256 id;
uint256 feedCounter;
uint256 lastTimeFed;
uint256 timeStaked;
bool expired;
}
struct Staker {
uint256 fundsDeposited;
// Amount of ERC721 Tokens staked
uint256 amountStaked;
// Last time of details update for this User
uint256 timeOfLastUpdate;
// Calculated, but unclaimed rewards for the User. The rewards are
// calculated each time the user writes to the Smart Contract
uint256 unclaimedRewards;
}
address[] public stakersArray;
// Mapping of User Address to Staker info
mapping(address => Staker) public stakers;
// Mapping of User Address to StakedNft
mapping(address => StakedNft[]) public stakedNfts;
// Mapping of Token Id to staker. Made for the SC to remember
// who to send back the ERC721 Token to.
mapping(uint256 => address) public stakerAddress;
mapping(uint256 => bool) public stakedBefore;
// food price
uint256[3] public foodPrices = [3500000000000000000, 7500000000000000000, 1500000000000000000];
//rewards
// 0 for Lemur, 1 for Rhino, 2 for Gorilla
// 0.6 = 6/10, 0.7 = 7/10, 0.8 = 8/10
uint256[3] public rewardsPerDay = [600000, 700000, 800000];
// 0 for Lemur, 1 for Rhino, 2 for Gorilla
uint256[3] public rewardDays = [500 days, 500 days, 500 days];
address public owner;
uint256 public ZooTokenDecimal = 1000000;
uint256 public whalesWithdrawalExtraFee = 2500000;
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call detected!");
locked = true;
_;
locked = false;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor(
INftCollection _nftCollection,
address __USDCToken,
ISingleSwap _SingleSwap,
IZooToken _ZooToken,
address _owner
) {
nftCollection = _nftCollection;
_USDCToken = __USDCToken;
USDCToken = IERC20USDC(__USDCToken);
swap = _SingleSwap;
owner = _owner;
ZooToken = _ZooToken;
}
event Withdrawn(uint256[] indexed _tokenIds, address _to);
event NewRewardsPerDay(uint256 _nftIndex, uint256 _newValue);
// // TokenId parameter in the feedYourAnimal function is used to specify the ID of the NFT that the user wants to feed
function feedYourAnimal(uint256 _tokenId) public {
// returns the index of the NFT based on the _tokenId parameter.
uint256 nftIndex = nftCollection.getIndexForId(_tokenId);
// fetches the price of food for the NFT at the given nftIndex.
uint256 foodPrice = foodPrices[nftIndex - 1];
// checks if the contract has been approved to spend the required amount of feeToken tokens by the caller
require( USDCToken.allowance(msg.sender, address(this)) >= foodPrice, "Approve Staking Contract");
// transfers the foodPrice amount of feeToken tokens from the caller to the contract.
USDCToken.transferFrom(msg.sender, address(this), foodPrice);
// fetches the array of StakedNft structs for the caller from the stakedNfts mapping.
StakedNft[] storage userStakedNfts = stakedNfts[msg.sender];
// stores the length of the userStakedNfts array.
uint256 len = userStakedNfts.length;
// iterates over each StakedNft struct in the userS takedNfts array.
for (uint256 i; i < len; i++) {
// checks if the _tokenId matches with the id of the StakedNft struct in the current iteration.
if (userStakedNfts[i].id == _tokenId) {
// increments the feedCounter property of the StakedNft struct by 1.
userStakedNfts[i].feedCounter += 1;
// updates the lastTimeFed property of the StakedNft struct with the current block timestamp.
userStakedNfts[i].lastTimeFed = block.timestamp;
// emits the AnimalFed event with the nftIndex, msg.sender, and foodPrice parameters.
// emit AnimalFed(nftIndex, msg.sender, foodPrice);
// exits the function once the if condition is met, meaning that the appropriate StakedNft struct has been found and updated.
return;
}
}
}
// function calculateRewards(address staker_) public view returns (uint256) {
// Staker memory staker = stakers[staker_];
// StakedNft[] memory _stakedNfts = stakedNfts[staker_];
// uint256 accRewards;
// for (uint256 i; i < staker.amountStaked; i++) {
// uint256 _index = nftCollection.getIndexForId(_stakedNfts[i].id);
// if (
// !_stakedNfts[i].expired &&
// stakerAddress[_stakedNfts[i].id] == staker_ &&
// (block.timestamp - _stakedNfts[i].timeStaked <= rewardDays[_index - 1])
// ) {
// uint256 TimeSinceLastFed = block.timestamp - _stakedNfts[i].lastTimeFed;
// uint256 daysOfRewards;
// if (TimeSinceLastFed <= 30 days) {
// daysOfRewards = block.timestamp - staker.timeOfLastUpdate;
// } else {
// uint256 timeNFTexpired = _stakedNfts[i].lastTimeFed + 30 days;
// if(staker.timeOfLastUpdate < timeNFTexpired){
// daysOfRewards = timeNFTexpired - staker.timeOfLastUpdate;
// }
// }
// if(daysOfRewards > 1 days){
// uint256 rewardRatePerDay = ((rewardsPerDay[_index - 1] / 100) * nftCollection.getFeeForId(_index))/ZooTokenDecimal;
// accRewards += (daysOfRewards * rewardRatePerDay) / 86400;
// }
// }
// }
// return accRewards;
// }
function calculateRewards(address staker_) public view returns (uint256) {
Staker memory staker = stakers[staker_];
StakedNft[] memory _stakedNfts = stakedNfts[staker_];
uint256 accRewards;
for (uint256 i; i < staker.amountStaked; i++) {
uint256 _index = nftCollection.getIndexForId(_stakedNfts[i].id);
uint256 daysOfRewards;
if (
!_stakedNfts[i].expired &&
stakerAddress[_stakedNfts[i].id] == staker_ &&
(block.timestamp - _stakedNfts[i].timeStaked <=
rewardDays[_index - 1])
) {
uint256 timeNFTexpired = _stakedNfts[i].lastTimeFed + 30 days;
if (timeNFTexpired > block.timestamp) {
daysOfRewards = block.timestamp - staker.timeOfLastUpdate;
} else {
daysOfRewards = timeNFTexpired - staker.timeOfLastUpdate;
}
if (daysOfRewards > 1 days) {
uint256 rewardRatePerDay = ((rewardsPerDay[_index - 1] /
100) * nftCollection.getFeeForId(_index)) /
ZooTokenDecimal;
accRewards += (daysOfRewards * rewardRatePerDay) / 86400;
}
}
}
return accRewards;
}
function updateUserPool(address staker_) internal {
Staker storage staker = stakers[staker_];
StakedNft[] storage _stakedNfts = stakedNfts[staker_];
for (uint256 i; i < staker.amountStaked; i++) {
uint256 _index = nftCollection.getIndexForId(_stakedNfts[i].id);
if (!_stakedNfts[i].expired) {
if (
stakerAddress[_stakedNfts[i].id] == staker_ &&
(block.timestamp - _stakedNfts[i].timeStaked <=
rewardDays[_index - 1])
) {
// Check if NFT has expired
if (
(block.timestamp - _stakedNfts[i].lastTimeFed) > 30 days
) {
_stakedNfts[i].expired = true;
}
}
}
}
}
function stakeNFT(uint256[] calldata _tokenIds) external nonReentrant {
if (stakers[msg.sender].amountStaked > 0) {
uint256 rewards = calculateRewards(msg.sender);
stakers[msg.sender].unclaimedRewards += rewards;
updateUserPool(msg.sender);
} else {
stakersArray.push(msg.sender);
}
uint256 len = _tokenIds.length;
// The loop iterates over the array of _tokenIds
for (uint256 i; i < len; ++i) {
//For each ERC721 token ID in the input array, it checks if the token has been staked before. If yes, it throws an error. as a token can only be staked once
require(!stakedBefore[_tokenIds[i]], "Can't Restake A Token");
uint256 index = nftCollection.getIndexForId(_tokenIds[i]);
// It also checks if the caller is the owner of the token
require(
nftCollection.ownerOf(_tokenIds[i]) == msg.sender,
"Can't Stake Token You Don't Own!"
);
require(
nftCollection.isApprovedForAll(msg.sender, address(this)),
"Approve Staker For The Nft"
);
nftCollection.transferFrom(msg.sender, address(this), _tokenIds[i]);
//here we will swap zootoken from 250 usdc
USDCToken.approve(
_USDCToken,
nftCollection.getFeeForId(index - 1) * 1e18
);
uint256 zoo_tokens = swap.SellingUSDCToken(
nftCollection.getFeeForId(index - 1) * 1e18
);
stakerAddress[_tokenIds[i]] = msg.sender;
StakedNft memory _stakedNft;
_stakedNft.id = _tokenIds[i];
_stakedNft.feedCounter = 0;
_stakedNft.lastTimeFed = block.timestamp;
_stakedNft.timeStaked = block.timestamp;
stakedNfts[msg.sender].push(_stakedNft);
stakers[msg.sender].fundsDeposited += zoo_tokens;
stakedBefore[_tokenIds[i]] = true;
}
// the amountStaked variable of the staker's struct is incremented by the number of tokens being staked (len), indicating that the staker has staked additional tokens
stakers[msg.sender].amountStaked += len;
// setting the current time
stakers[msg.sender].timeOfLastUpdate = block.timestamp;
}
function claimRewards() external {
uint256 rewards = calculateRewards(msg.sender) +
stakers[msg.sender].unclaimedRewards;
updateUserPool(msg.sender);
// require(rewards > 0, "You have no rewards to claim");
// stakers[msg.sender].timeOfLastUpdate = block.timestamp;
// stakers[msg.sender].unclaimedRewards = 0;
// ZooToken.transfer(msg.sender, rewards);
}
function getWhaleFee(uint256 _userFunds) public view returns (uint256) {
uint256 rewardTokenBalance = ZooToken.balanceOf(address(this));
if (
_userFunds <
((((1 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return 0;
} else if (
_userFunds <
((((2 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * whalesWithdrawalExtraFee) /
ZooTokenDecimal;
} else if (
_userFunds <
((((3 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 2)) /
ZooTokenDecimal;
} else if (
_userFunds <
((((4 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 3)) /
ZooTokenDecimal;
} else if (
_userFunds <
((((5 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 4)) /
ZooTokenDecimal;
} else if (
_userFunds <
((((6 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 5)) /
ZooTokenDecimal;
} else if (
_userFunds <
((((7 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 6)) /
ZooTokenDecimal;
} else if (
_userFunds <
((((8 * ZooTokenDecimal) / 100) * rewardTokenBalance) /
ZooTokenDecimal)
) {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 7)) /
ZooTokenDecimal;
} else {
return
((_userFunds / 100) * (whalesWithdrawalExtraFee * 8)) /
ZooTokenDecimal;
}
}
function withdraw(uint256[] calldata _tokenIds) external nonReentrant {
Staker storage __staker = stakers[msg.sender];
require(__staker.amountStaked > 0, "You Have No Tokens Staked");
uint256 rewards = calculateRewards(msg.sender);
updateUserPool(msg.sender);
uint256 userFunds = __staker.fundsDeposited;
// Check if whale txn and deduce tax accordingly
uint256 whaleFee = getWhaleFee(userFunds);
uint256 len = _tokenIds.length;
__staker.unclaimedRewards += rewards - (whaleFee * len);
for (uint256 i; i < len; ++i) {
require(stakerAddress[_tokenIds[i]] == msg.sender);
stakerAddress[_tokenIds[i]] = address(0);
nftCollection.transferFrom(address(this), msg.sender, _tokenIds[i]);
}
__staker.amountStaked -= len;
__staker.timeOfLastUpdate = block.timestamp;
//poping out user's tokens from stakersArray if it exist.
if (__staker.amountStaked == 0) {
for (uint256 i; i < stakersArray.length; ++i) {
if (stakersArray[i] == msg.sender) {
stakersArray[i] = stakersArray[stakersArray.length - 1]; //swapping
stakersArray.pop();
}
}
}
emit Withdrawn(_tokenIds, msg.sender);
}
function setSwapAddress(address _swapAddress) public onlyOwner {
require(_swapAddress != address(0), "address is undefined");
swap = ISingleSwap(_swapAddress);
}
function setUsdcAddress(address usdcToken) public onlyOwner {
require(usdcToken != address(0), "address is undefined");
_USDCToken = usdcToken;
}
function setNFTAddress(address nftAddress) public onlyOwner {
require(nftAddress != address(0), "address is undefined");
nftCollection = INftCollection(nftAddress);
}
function setZooAddress(address ZooAddress) public onlyOwner {
require(ZooAddress != address(0), "address is undefined");
ZooToken = IZooToken(ZooAddress);
}
function setZooTokenDecimal(uint256 tokenDecimal) public onlyOwner {
require(tokenDecimal >= 10, "should be atleast 1 decimal");
ZooTokenDecimal = tokenDecimal;
}
function setwhalesWithdrawalExtraFee(uint256 fees) public onlyOwner {
require(fees > 0, "value is undefined");
whalesWithdrawalExtraFee = fees;
}
function getSwapAddress() public view returns (ISingleSwap) {
return swap;
}
function getUsdcAddress() public view returns (address) {
return _USDCToken;
}
function getNFTAddress() public view returns (INftCollection) {
return nftCollection;
}
function getZooAddress() public view returns (IZooToken) {
return ZooToken;
}
function getZooTokenDecimal() public view returns (uint256) {
return ZooTokenDecimal;
}
function getwhalesWithdrawalExtraFee() public view returns (uint256) {
return whalesWithdrawalExtraFee;
}
function setRewardsPerDay(
uint256 _nftIndex,
uint256 _newValue
) public onlyOwner {
require(_nftIndex > 0 && _nftIndex < 4, "Invalid Index");
address[] memory _stakers = stakersArray;
uint256 len = _stakers.length;
for (uint256 i; i < len; ++i) {
address user = _stakers[i];
stakers[user].unclaimedRewards += calculateRewards(user);
updateUserPool(user);
stakers[user].timeOfLastUpdate = block.timestamp;
}
rewardsPerDay[_nftIndex - 1] = _newValue;
emit NewRewardsPerDay(_nftIndex, _newValue);
}
function getRewardsPerDay()
public
view
returns (uint256, uint256, uint256)
{
return (rewardsPerDay[0], rewardsPerDay[0], rewardsPerDay[0]);
}
function setRewardDays(
uint256 _nftIndex,
uint256 _newDays
) public onlyOwner {
require(_nftIndex > 0 && _nftIndex < 4, "Invalid Index");
address[] memory _stakers = stakersArray;
uint256 len = _stakers.length;
for (uint256 i; i < len; ++i) {
address user = _stakers[i];
stakers[user].unclaimedRewards += calculateRewards(user);
updateUserPool(user);
stakers[user].timeOfLastUpdate = block.timestamp;
}
rewardDays[_nftIndex - 1] = _newDays;
// emit NewRewardDays(_nftIndex, _newDays);
}
function getRewardDays()
public
view
returns (uint256, uint256, uint256)
{
return (rewardDays[0], rewardDays[0], rewardDays[0]);
}
function userStakeInfo(
address _user
) public view returns (uint256 _tokensStaked, uint256 _availableRewards) {
return (stakers[_user].amountStaked, availableRewards(_user));
}
function availableRewards(address _user) public view returns (uint256) {
if (stakers[_user].amountStaked == 0) {
return stakers[_user].unclaimedRewards;
}
uint256 _rewards = stakers[_user].unclaimedRewards +
calculateRewards(_user);
return _rewards;
}
function isHungry(uint256 _tokenId) public view returns (bool) {
bool _isHungry;
address _staker = stakerAddress[_tokenId];
require(_staker != address(0), "Nft Is Not Staked");
Staker memory staker = stakers[_staker];
StakedNft[] memory _stakedNfts = stakedNfts[_staker];
for (uint256 i; i < staker.amountStaked; i++) {
StakedNft memory _stakedNft = _stakedNfts[i];
if (_stakedNft.id == _tokenId) {
return (block.timestamp - _stakedNft.lastTimeFed) > 30 days;
}
}
return _isHungry;
}
}
// address public constant USDC_address = 0x0FA8781a83E46826621b3BC094Ea2A0212e71B23;
Last updated