Solidity NFT bird feeding(Level) + Staking
Masood Ur Rehman
Posted on November 1, 2022
Advance Solidity experince required
I will get straight to the point.
This is an nft project, in which, birds(nft's) are upgraded by feeding them ERC20 tokens. Also, each bird level will have his own reward APY.
PROCESS:-
Egg mint > egg in incubator > egg hatched + random attributes + random uniqueness(common, uncommon, rare, legendary) > bird upgrade to mature > mature to max mature.
reward will depend on bird level and uniqueness.
So let's start.
/ SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./base64.sol";
they are libraries which we will need. Most of them are self explanatory. The base64 library is needed for text encoding. you will see it later.
contract Astrobirdz is ERC721, Ownable {
using SafeMath for uint256;
using Counters for Counters.Counter;
Counters.Counter public tokenIds;
address private _burnAddress = 0x000000000000000000000000000000000000dEaD;
address private _marketPlaceAddress;
address private _tokenAddress;
string private _eggUri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/video_2022-04-15_14-40-52.mp4";
The contract name is Astrobirdz. Counters
library is used with the tokenIds
variable, so we will be able to increment or decrement tokenIds
value safely, like, tokenIds.increment()
etc.
_burnAddress
is a dead address, where some percent of tokens will be burned.
_marketPlaceAddress
is the market place address where birds(Eggs) can be sold by the contract owner.
_tokenAddress
is the erc20 address which will be used to feed(upgrade the birds).
_eggUri
is the url for the egg nft, which will be minted to the owner, which he will be able to sell in marketplace.
// Rarity Classes
enum Class {
Common,
Uncommon,
Rare,
Legendary
}
uint private _seed;
uint8 private _burnPercent = 25;
uint8 constant NUM_CLASSES = 4;
// Starts From 0
uint8 private constant UNIQUENFTS = 6;
uint8 public _commonMatureAPY = 10;
uint8 public _commonMaxMatureAPY = 15;
uint8 public _unCommonMatureAPY = 15;
uint8 public _unCommonMaxMatureAPY = 20;
uint8 public _rareMatureAPY = 25;
uint8 public _rareMaxMatureAPY = 30;
uint8 public _legendaryMatureAPY = 50;
uint8 public _legendaryMaxMatureAPY = 70;
uint public commonMatureCost = 30000 * 10**18;
uint public commonMaxMatureCost = 50000 * 10**18;
uint public unCommonMatureCost = 50000 * 10**18;
uint public unCommonMaxMatureCost = 70000 * 10**18;
uint public rareMatureCost = 100000 * 10**18;
uint public rareMaxMatureCost = 150000 * 10**18;
uint public legendaryMatureCost = 150000 * 10**18;
uint public legendaryMaxMatureCost = 200000 * 10**18;
There will be 4 classes of birds rarity. there will be 6 unique nft birds, like eagles, vultures etc. the Apy is the reward and cost are the level upgrading erc20 tokens price.
struct Attributes {
string uniqueAttribute;
uint8 speice;
uint8 rarity;
uint8 cannon;
uint8 laser;
uint8 bomb;
uint8 shields;
uint8 armour;
uint8 health;
//check if attributes are setted
bool set;
}
It will have the following numbers of attributes(powers etc).
struct EggHatch {
uint hatchTime;
bool hasAlreadyHatched;
bool isHatching;
}
mapping(uint=>Attributes) private _tokenIdToAttributes;
mapping(uint=>EggHatch) private _eggHatch;
mapping(uint=>string) private _nftToUniqueAttr;
This code is self explanatory. EggHatch
is to keep track of egg hatching when it will be placed in incubator.
mapping(uint=>uint) public level;
mapping(uint=>uint) private _rewardTime;
event EggMinted(address indexed, uint indexed);
event EggLocked(uint indexed, uint indexed);
event EggRarity(uint indexed, uint indexed);
event UpgradeMature(uint indexed, uint indexed);
event UpgradeMaxMature(uint indexed, uint indexed);
event Reward(uint indexed, uint indexed, uint indexed);
level
is used to keep track of bird level, whether it's mature or max mature etc. _rewardTime
is for how much time has been passed since the reward was claimed.
constructor(address tokenAddress, address _marketAddress) ERC721("Astrobirdz", "ABZ") {
_tokenAddress = tokenAddress;
_marketPlaceAddress = _marketAddress;
}
Set the constructor.
function mintEgg(uint tNumber)
public
onlyOwner
{
for(uint i = 0; i<tNumber; i++) {
tokenIds.increment();
uint256 newItemId = tokenIds.current();
_mint(msg.sender, newItemId);
setApprovalForAll(_marketPlaceAddress, true);
level[newItemId] = 0;
}
emit EggMinted(msg.sender, tNumber);
}
Eggs are minted only by the owner, approval is also given to the marketplaceAddress
, so afterwards owner doesn't have to approve every egg manually for selling it on marketplace.
function lockInIncubator(uint _tokenId) public {
require(ownerOf(_tokenId) == msg.sender, "Not Owner");
EggHatch memory eggHatch = _eggHatch[_tokenId];
require(eggHatch.hasAlreadyHatched == false, "already hatched");
eggHatch.isHatching = true;
eggHatch.hasAlreadyHatched = true;
eggHatch.hatchTime = block.timestamp + 7 days;
_eggHatch[_tokenId] = eggHatch;
emit EggLocked(_tokenId, eggHatch.hatchTime);
}
this function will lock the egg in incubator for 7 days, after that it can be hatched.
first it will check if the owner of the egg is the msg.sender
, then it will check if it's already been hatched or not.
it will then set isHatching
and hasAlreadyHatched
to true
.
it will also set 7 days for it to be in incubator.
function hatchEgg(uint _tokenId) public {
require(ownerOf(_tokenId) == msg.sender, "Not Owner");
EggHatch memory eggHatch = _eggHatch[_tokenId];
require(eggHatch.isHatching == true, "Not Hatching");
require(eggHatch.hatchTime <= block.timestamp,"Hatch Time Hasn't Passed Yet");
eggHatch.isHatching = false;
_eggHatch[_tokenId] = eggHatch;
level[_tokenId] = 1;
Attributes memory _attr = selectRandomNftWithAttributes(_tokenId);
_attr = selectAttrbiutes(_attr);
_tokenIdToAttributes[_tokenId] = _attr;
emit EggRarity(_tokenId, _attr.rarity);
}
this function is for hatching the egg after 7 days has passed.
the require statements are self explanatory.
level
is set to 1, selectRandomNftWithAttributes
will select random bird from 6 unique birds, and assign one unique attribute like Speed, Camoflauge, Strength etc.
selectAttrbiutes
will give rarity to the bird whether it will be common, uncommon, rare, legendary according to probability of each rarity, and also set the attributes randomly, we will see this function a little later.
function selectRandomNftWithAttributes(uint _tokenId) internal returns(Attributes memory) {
uint _rand = randomUniqueNft();
Attributes memory _attr = _tokenIdToAttributes[_tokenId];
if(_rand == 0) {
_attr.uniqueAttribute = "Powerful Sharp Feet";
_attr.speice = 0;
} else if(_rand == 1) {
_attr.uniqueAttribute = "Powerful Beak";
_attr.speice = 1;
} else if(_rand == 2) {
_attr.uniqueAttribute = "Speed";
_attr.speice = 2;
} else if(_rand == 3) {
_attr.uniqueAttribute = "Camoflauge";
_attr.speice = 3;
} else if(_rand == 4) {
_attr.uniqueAttribute = "Strength";
_attr.speice = 4;
} else if(_rand == 5) {
_attr.uniqueAttribute = "Intelligence";
_attr.speice = 5;
}
return _attr;
}
as it was previously mentioned that this will select a random unique bird from six birds. and it will assign one unique attribute according to which bird was selected by the randomUniqueNft
function.
function randomUniqueNft() internal view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, _seed)));
return rand % UNIQUENFTS;
}
it's a simple function to return one unique bird from six birds.
function selectAttrbiutes(Attributes memory attr) internal view returns(Attributes memory){
Class _class = randomNumProb();
if(_class == Class.Common) {
attr.rarity = 0;
attr.cannon = randRarity(230, 34);
attr.laser = randRarity(10230, 34);
attr.bomb = randRarity(12200, 34);
attr.shields = randRarity(10560, 34);
attr.armour = randRarity(10740, 34);
attr.health = randRarity(10450, 34);
attr.set = true;
return attr;
} else if(_class == Class.Uncommon) {
attr.rarity = 1;
attr.cannon = randRarity(230, 15) + 35;
attr.laser = randRarity(10230, 15) + 35;
attr.bomb = randRarity(12200, 15) + 35;
attr.shields = randRarity(10560, 15) + 35;
attr.armour = randRarity(10740, 15) + 35;
attr.health = randRarity(10450, 15) + 35;
attr.set = true;
return attr;
} else if(_class == Class.Rare) {
attr.rarity = 2;
attr.cannon = randRarity(230, 25) + 50;
attr.laser = randRarity(10230, 25) + 50;
attr.bomb = randRarity(12200, 25) + 50;
attr.shields = randRarity(10560, 25) + 50;
attr.armour = randRarity(10740, 25) + 50;
attr.health = randRarity(10450, 25) + 50;
attr.set = true;
return attr;
} else if(_class == Class.Legendary) {
attr.rarity = 3;
attr.cannon = randRarity(230, 25) + 75;
attr.laser = randRarity(10230, 25) + 75;
attr.bomb = randRarity(12200, 25) + 75;
attr.shields = randRarity(10560, 25) + 75;
attr.armour = randRarity(10740, 25) + 75;
attr.health = randRarity(10450, 25) + 75;
attr.set = true;
return attr;
}
}
it might look overwhelming but it's simple logic.
it will first select random class, like it's common, uncommon etc.
the attributes are set according the rarity of class, common will have low attributes, and legendary will have max attributes then others.
function randomNumProb() internal view returns(Class) {
uint rand = uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, _seed))) % 100;
uint[] memory _classProbabilities = new uint[](4);
_classProbabilities[0] = 68;
_classProbabilities[1] = 20;
_classProbabilities[2] = 10;
_classProbabilities[3] = 2;
// Start at top class (length - 1)
// skip common (0), we default to it
for (uint i = _classProbabilities.length - 1; i > 0; i--) {
uint probability = _classProbabilities[i];
if(rand < probability) {
return Class(i);
} else {
rand = rand - probability;
}
}
return Class.Common;
}
it will return a random class rarity according to the probability.
function upgradeToMatureBird(uint _tokenId) external {
require(ownerOf(_tokenId) == msg.sender, "not owner");
require(level[_tokenId] == 1, "not baby bird, only baby bird can be upgraded");
IERC20 token = IERC20(_tokenAddress);
Attributes memory attr = _tokenIdToAttributes[_tokenId];
uint8 rar = attr.rarity;
uint cost;
if(rar == 0) {
cost = commonMatureCost;
} else if(rar == 1) {
cost = unCommonMatureCost;
} else if(rar == 2) {
cost = rareMatureCost;
} else if(rar == 3) {
cost = legendaryMatureCost;
}
uint balance = token.balanceOf(msg.sender);
require(balance >= cost, "low balance");
uint256 allowance = token.allowance(msg.sender, address(this));
require(allowance >= cost, "Check the token allowance");
uint burnAmount = cost.mul(_burnPercent).div(100);
token.transferFrom(msg.sender, address(this), cost);
token.transfer(_burnAddress, burnAmount);
level[_tokenId] = 2;
_rewardTime[_tokenId] = block.timestamp;
emit UpgradeMature(_tokenId, cost);
}
birds are upgraded to level 2
using this function. 25% are burned, the rest are sent to the contract. the if else statement
checks whether the rarity is common, uncommon, rare, legendary. the cost to upgrade will depend on the rarity.
function upgradeToMaxMatureBird(uint _tokenId) external {
require(ownerOf(_tokenId) == msg.sender, "not owner");
require(level[_tokenId] == 2, "not mature bird, only mature bird can be upgraded");
IERC20 token = IERC20(_tokenAddress);
Attributes memory attr = _tokenIdToAttributes[_tokenId];
uint8 rar = attr.rarity;
uint cost;
if(rar == 0) {
cost = commonMaxMatureCost;
} else if(rar == 1) {
cost = unCommonMaxMatureCost;
} else if(rar == 2) {
cost = rareMaxMatureCost;
} else if(rar == 3) {
cost = legendaryMaxMatureCost;
}
uint balance = token.balanceOf(msg.sender);
require(balance >= cost, "low balance");
uint256 allowance = token.allowance(msg.sender, address(this));
require(allowance >= cost, "Check the token allowance");
uint burnAmount = cost.mul(_burnPercent).div(100);
uint remainingTokens = cost - burnAmount;
token.transferFrom(msg.sender, address(this), remainingTokens);
token.transfer(_burnAddress, burnAmount);
level[_tokenId] = 3;
withdrawReward(_tokenId);
emit UpgradeMaxMature(_tokenId, cost);
}
level of bird is upgraded to 3. same logic as the other upgradeMature
function.
// this need to be fixed, withdrawReward function contains some bugs
function withdrawReward(uint _tokenId) public returns(uint) {
require(ownerOf(_tokenId) == msg.sender, "not Owner");
require(level[_tokenId] > 1, "only mature and max mature bird can withdraw");
Attributes memory attr = _tokenIdToAttributes[_tokenId];
uint per;
if(attr.rarity == 0) {
if(level[_tokenId] == 2) {
per = _commonMatureAPY;
} else if(level[_tokenId] == 3) {
per = _commonMaxMatureAPY;
}
} else if(attr.rarity == 1) {
if(level[_tokenId] == 2) {
per = _unCommonMatureAPY;
} else if(level[_tokenId] == 3) {
per = _unCommonMaxMatureAPY;
}
} else if(attr.rarity == 2) {
if(level[_tokenId] == 2) {
per = _rareMatureAPY;
} else if(level[_tokenId] == 3) {
per = _rareMaxMatureAPY;
}
} else if(attr.rarity == 3) {
if(level[_tokenId] == 2) {
per = _legendaryMatureAPY;
} else if(level[_tokenId] == 3) {
per = _legendaryMaxMatureAPY;
}
}
per = per * 1000000000;
uint perInSec = per / 31536000;
uint bal = IERC20(_tokenAddress).balanceOf(address(this));
bal = bal.div(1000000000);
uint r = bal.mul(perInSec).div(100);
uint t = (block.timestamp).sub(_rewardTime[_tokenId]);
r = r.mul(t);
IERC20(_tokenAddress).transfer(msg.sender, r);
_rewardTime[_tokenId] = block.timestamp;
emit Reward(_tokenId, t, r);
return r;
}
So I am not so good in math, and for sure this function may contain bugs. so you might want to change the reward system to weeks rather seconds. the seconds wihdraw system is confusing for me. if somebody fix this, then do a PR.
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k-1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
uint to string conversion function. Will need it during tokenUri
function.
function tokenURI(uint256 tokenId) override(ERC721) public view returns (string memory) {
if(_tokenIdToAttributes[tokenId].set == false) {
string memory json = Base64.encode(
bytes(string(
abi.encodePacked(
'{"name": "', uint2str(tokenId), '",',
'"image_data": "', _eggUri, '",',
'"description": "', 'An Egg"',
'}'
)
))
);
return string(abi.encodePacked('data:application/json;base64,', json));
}
string memory uri = "";
if(level[tokenId] == 1) {
if(_tokenIdToAttributes[tokenId].speice == 0) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/baby-eagle-complete.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 1) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Baby%20-%20Cockatiel.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 2) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Baby%20-%20Sparrow.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 3) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Baby%20-%20Cardinal.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 4) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Baby%20-%20Vulture.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 5) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Baby%20-%20Swan.mp4";
}
} else if(level[tokenId] == 2 || level[tokenId] == 3) {
if(_tokenIdToAttributes[tokenId].speice == 0) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Golden%20Eagle.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 1) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Cockateil.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 2) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Sparrow.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 3) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Cardinal.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 4) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Vulture.mp4";
} else if(_tokenIdToAttributes[tokenId].speice == 5) {
uri = "https://gateway.pinata.cloud/ipfs/QmVWCtAxaRVktazv4JddXMhMZYAUNRWrvZoDGQhmuy64Hp/Adult%20-%20Swan.mp4";
}
}
string memory json = Base64.encode(
bytes(string(
abi.encodePacked(
'{"name": "', uint2str(tokenId), '",',
'"image_data": "', uri, '",',
// '"description": "', 'Bird"', ',',
'"attributes": [{"trait_type": "Cannon", "value": "', uint2str(_tokenIdToAttributes[tokenId].cannon), '"},',
'{"trait_type": "Attribute", "value": "', _tokenIdToAttributes[tokenId].uniqueAttribute, '"},',
'{"trait_type": "Laser", "value": "', uint2str(_tokenIdToAttributes[tokenId].laser), '"},',
'{"trait_type": "Bomb", "value": "', uint2str(_tokenIdToAttributes[tokenId].bomb), '"},',
'{"trait_type": "Shields", "value": "', uint2str(_tokenIdToAttributes[tokenId].shields), '"},',
'{"trait_type": "Armour", "value": "', uint2str(_tokenIdToAttributes[tokenId].armour), '"},',
'{"trait_type": "Health", "value": "', uint2str(_tokenIdToAttributes[tokenId].health), '"}',
']}'
)
))
);
return string(abi.encodePacked('data:application/json;base64,', json));
}
this function just sets the attributes to json format, standard for the erc721 tokeUri. nothing completed here, only conditions are used to correctly display the egg or bird according to the level.
Posted on November 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.