You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

730 lines
25 KiB

// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.14;
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/Base64Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/ITreasury.sol";
import "./interfaces/IPancakeSwapPair.sol";
import "./mixins/signature-control.sol";
contract NFT is ERC721EnumerableUpgradeable, SignatureControl {
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet;
using SafeERC20Upgradeable for IERC20Upgradeable;
//variables
bool pause;
uint256 nonce;
ITreasury treasury;
IERC20Upgradeable BoM;
address redTrustFund;
IPancakeSwapPair public pairContract;
IPancakeSwapRouter public swapRouter;
uint256 treasuryFee;
uint256 redTrustFee;
uint256 rewardPoolFee;
uint256 supply;
uint256 wethInRewardPool;
uint256 busdInLotteryPool;
mapping(uint256 => uint256) mintCapOfToken;
mapping(uint256 => address[]) tokenIdToType;
mapping(address => mapping(uint256 => uint256)) tokensOwnedOfType;
mapping(uint256 => bool) usedNonces;
//mint prices, caps, addresses of reward tokens(shiba,floki,doggy,doge)
uint256[4] prices = [
.001 ether,
// 300 * 10**18,
250 * 10**18,
200 * 10**18,
250 * 10**18
];
IERC20Upgradeable BUSD;
IERC20Upgradeable WETH;
mapping(address => uint256) tokenToPrices;
uint256[4] nftCaps = [1000, 1500, 2000, 1500];
address[4] public rewardTokens = [
0x0000000000000000000000000000000000000000,
0x0000000000000000000000000000000000000001,
0x0000000000000000000000000000000000000002,
0x0000000000000000000000000000000000000003
];
mapping(address => uint256) addressToType;
mapping(address => uint256) totalRatesForType;
mapping(address => mapping(address => uint256)) addrerssToRatesForType;
uint256[5] rewardPoolForToken;
uint256[5] rewardsPerShareStored;
uint256[5] lastUpdatePools;
mapping(address => uint256[5]) accountShares;
uint256[5] totalShares;
mapping(address => mapping(uint256 => uint256)) accountRewards;
mapping(address => mapping(uint256 => uint256)) accountRewardsPerTokenPaid;
//whitelist shit
bool public isPresale = true;
uint256 whitelistPrice = 200 ether;
uint256 maxMintPerAddressDuringWhitelist = 5;
uint256 whitelistMintCapPerType = 250;
mapping(address => bool) isWhitelisted;
mapping(address => uint256) mintedOnWhitelist;
mapping(uint256 => uint256) whitelistMintCapOfToken;
EnumerableSetUpgradeable.AddressSet _holders; // TODO: migrate?
uint256 statMultiplier = 100000;
mapping(uint256 => TokenInfo) tokenIdToInfo;
struct TokenInfo {
string image;
address[] tokens;
uint256 rate;
uint256 attack;
uint256 defense;
uint256 health;
uint256 critChance;
uint256 critDmg;
uint256 recover;
}
mapping(address => Tokenomic) addressToToken;
struct Tokenomic {
uint256 stakingRate;
uint256 attack;
uint256 defense;
uint256 health;
uint256 critChance;
uint256 critDmg;
uint256 recovery;
}
event TokenMinted(
address indexed minter,
uint256 tokenId,
address token,
uint256 rate
);
event TokenCombined(
address indexed user,
uint256[] burntTokens,
address[] tokens,
uint256 tokenId,
uint256 rate
);
event RewardPoolRaised(uint256 amount);
event LotteryPoolRaised(uint256 amount);
//modifiers
modifier onlyAdmin() {
require(treasury.isAdmin(msg.sender), "!admin");
_;
}
modifier isUnpaused() {
require(!pause, "paused");
_;
}
modifier isValidWhitelistMint(uint256 amount, uint256 tokenType) {
require(isPresale, "!presale");
require(isWhitelisted[msg.sender], "!whitelisted");
require(
mintedOnWhitelist[msg.sender] + amount <=
maxMintPerAddressDuringWhitelist, "maxMint exceeded"
);
require(tokenType <= 3, "!tokenType");
require(
whitelistMintCapOfToken[tokenType] + amount <=
whitelistMintCapPerType, "mintCap exceeded"
);
_;
}
modifier isValidMint(uint256 amount, uint256 tokenType) {
require(!isPresale, "(mint) presale");
require(tokenType <= 3, "!tokenType");
require(mintCapOfToken[tokenType] + amount <= nftCaps[tokenType]);
_;
}
modifier isValidCombine(uint256[] memory tokenIds) {
require(tokenIds.length > 1 && tokenIds.length <= 4, "wrong tokenIds length");
bool hasDupes;
for (uint256 i = 0; i < tokenIds.length; i++) {
require(ownerOf(tokenIds[i]) == msg.sender, "!owner");
require(tokenIdToType[tokenIds[i]].length == 1, "!tokenIdToType");
for (uint256 index = i; index < tokenIds.length; index++) {
if (
index != i &&
tokenIdToType[tokenIds[i]][0] ==
tokenIdToType[tokenIds[index]][0]
) {
hasDupes = true;
}
}
require(!hasDupes, "dupes");
}
_;
}
function initialize(
string memory name,
string memory symbol,
ITreasury _treasury,
address _redTrustFund,
IERC20Upgradeable token,
uint256 _rewardFee,
uint256 _treasuryFee,
uint256 _redTrustFee,
IPancakeSwapPair _pair,
IPancakeSwapRouter _router,
IERC20Upgradeable _BUSD,
IERC20Upgradeable _WETH
) public initializer {
treasury = _treasury;
BoM = token;
__ERC721_init(name, symbol);
redTrustFund = _redTrustFund;
rewardPoolFee = _rewardFee;
redTrustFee = _redTrustFee;
treasuryFee = _treasuryFee;
uint16[4] memory attack = [1000, 1000, 700, 850];
uint16[4] memory defense = [800, 1000, 850, 1000];
uint16[4] memory health = [800, 800, 1000, 850];
uint16[4] memory critChance = [400, 200, 400, 300];
uint16[4] memory critDmg = [600, 500, 800, 500];
uint16[4] memory recovery = [600, 700, 1000, 900];
for (uint256 i = 0; i < 4; i++) {
addressToToken[rewardTokens[i]].attack = attack[i];
addressToToken[rewardTokens[i]].defense = defense[i];
addressToToken[rewardTokens[i]].health = health[i];
addressToToken[rewardTokens[i]].critChance = critChance[i];
addressToToken[rewardTokens[i]].critDmg = critDmg[i];
addressToToken[rewardTokens[i]].recovery = recovery[i];
tokenToPrices[rewardTokens[i]] = prices[i];
addressToType[rewardTokens[i]] = i;
}
pairContract = _pair;
swapRouter = _router;
BUSD = _BUSD;
WETH = _WETH;
}
function addWhitelist(address[] memory whitelist) public onlyAdmin {
for (uint256 i = 0; i < whitelist.length; i++) {
isWhitelisted[whitelist[i]] = true;
}
}
function mintPresale(uint256 amount, uint256 tokenType)
public
isUnpaused
isValidWhitelistMint(amount, tokenType)
{
for (uint256 i = 0; i < amount; i++) {
supply++;
tokenIdToInfo[supply].tokens.push(rewardTokens[tokenType]);
tokenIdToType[supply].push(rewardTokens[tokenType]);
uint256 rate = createTokenStats(supply);
_mint(msg.sender, supply);
emit TokenMinted(msg.sender, supply, rewardTokens[tokenType], rate);
}
mintedOnWhitelist[msg.sender] += amount;
whitelistMintCapOfToken[tokenType] += amount;
mintCapOfToken[tokenType] += amount;
//TBD: swap to bnb
_distributeFunds(amount * whitelistPrice);
}
function mint(uint256 amount, uint256 tokenType)
public
isUnpaused
isValidMint(amount, tokenType)
{
for (uint256 i = 0; i < amount; i++) {
supply++;
tokenIdToInfo[supply].tokens.push(rewardTokens[tokenType]);
tokenIdToType[supply].push(rewardTokens[tokenType]);
uint256 rate = createTokenStats(supply);
_mint(msg.sender, supply);
emit TokenMinted(msg.sender, supply, rewardTokens[tokenType], rate);
}
mintCapOfToken[tokenType] += amount;
//TBD: swap to bnb
_distributeFunds(amount * prices[tokenType]);
}
function combineTokens(uint256[] memory tokenIds)
public
payable
isUnpaused
{
require(msg.value == 0.05 ether, "!value");
supply++;
uint256 price;
address[] memory tokens = new address[](tokenIds.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
TokenInfo memory token = tokenIdToInfo[tokenIds[i]];
address intermediateStorageForToken = tokenIdToType[tokenIds[i]][0];
tokenIdToInfo[supply].tokens.push(intermediateStorageForToken);
tokenIdToType[supply].push(intermediateStorageForToken);
tokens[i] = intermediateStorageForToken;
_burn(tokenIds[i]);
price += tokenToPrices[tokenIdToType[tokenIds[i]][0]] / 4;
for (uint256 index = 0; index < token.tokens.length; index++) {
totalRatesForType[token.tokens[index]] -= token.rate;
}
}
uint256 rate = createTokenStats(supply);
_mint(msg.sender, supply);
emit TokenCombined(msg.sender, tokenIds, tokens, supply, rate);
//TBD: calculate price with usd in mind
_distributeFunds(price);
}
function createTokenStats(uint256 tokenId) internal returns (uint256 rate) {
TokenInfo storage token = tokenIdToInfo[tokenId];
uint256[7] memory seeds;
for (uint8 i = 0; i < 7; i++) {
nonce++;
seeds[i] = (
uint256(
keccak256(
abi.encodePacked(msg.sender, block.timestamp, nonce)
)
)
);
}
if (token.tokens.length == 1) {
uint256 rateMod = (seeds[0] % 100) + 1;
if (rateMod == 1) {
token.rate = 2000;
} else if (rateMod <= 3) {
token.rate = 1750;
} else if (rateMod <= 10) {
token.rate = 1500;
} else if (rateMod <= 20) {
token.rate = 1250;
} else {
token.rate = 1000;
}
} else if (token.tokens.length == 2) {
token.rate = 2250;
} else if (token.tokens.length == 3) {
token.rate = 2500;
} else {
token.rate = 3000;
}
rate = token.rate;
for (uint256 i = 0; i < token.tokens.length; i++) {
totalRatesForType[token.tokens[i]] += token.rate;
}
createBattleStats(seeds, token, getStatMultipliers(token));
}
function createBattleStats(
uint256[7] memory seeds,
TokenInfo storage token,
uint256[6] memory statMultipliers
) internal {
token.attack =
(((seeds[1] % 71) + 30) * (token.rate * statMultipliers[0])) /
statMultiplier;
token.defense =
(((seeds[2] % 71) + 30) * (token.rate * statMultipliers[1])) /
statMultiplier;
token.health =
(((seeds[3] % 51) + 50) * (token.rate * statMultipliers[2])) /
statMultiplier;
token.critChance =
(((seeds[4] % 41) + 30) * (token.rate * statMultipliers[3])) /
statMultiplier;
token.critDmg =
(((seeds[5] % 61) + 10) * (token.rate * statMultipliers[4])) /
statMultiplier;
token.recover =
(((seeds[6] % 51) + 50) * (token.rate * statMultipliers[5])) /
statMultiplier;
}
function getStatMultipliers(TokenInfo storage token)
internal
view
returns (uint256[6] memory statMultipliers)
{
uint16[4] memory megaStatMultipliers = [1000, 1250, 1500, 2000];
for (uint8 i = 0; i < token.tokens.length; i++) {
Tokenomic memory tokenomic = addressToToken[token.tokens[i]];
statMultipliers[0] += tokenomic.attack;
statMultipliers[1] += tokenomic.defense;
statMultipliers[2] += tokenomic.health;
statMultipliers[3] += tokenomic.critChance;
statMultipliers[4] += tokenomic.critDmg;
statMultipliers[5] += tokenomic.recovery;
}
for (uint8 i = 0; i < 6; i++) {
statMultipliers[i] =
(statMultipliers[i] / token.tokens.length) *
megaStatMultipliers[token.tokens.length - 1];
}
}
function _distributeFunds(uint256 amount) internal {
uint256[] memory requiredAmountOfTokens = _getAmountsIn(
amount,
address(BoM),
address(WETH)
);
require(
BoM.allowance(msg.sender, address(this)) >=
requiredAmountOfTokens[0],
"allowance too low"
);
BoM.transferFrom(msg.sender, address(this), requiredAmountOfTokens[0]);
uint256 bnbAmount = _swap(
amount,
requiredAmountOfTokens[0],
address(BoM),
address(WETH)
)[0];
uint256 _treasuryFee = (bnbAmount * treasuryFee) / 1000;
uint256 _redTrustFee = (bnbAmount * redTrustFee) / 1000;
uint256 _rewardPoolFee = bnbAmount - _treasuryFee - _redTrustFee;
BoM.transferFrom(msg.sender, address(treasury), _treasuryFee);
BoM.transferFrom(msg.sender, redTrustFund, _redTrustFee);
BoM.transferFrom(msg.sender, address(this), _rewardPoolFee);
_addToPool(_rewardPoolFee);
}
function lottery() public onlyAdmin {
require(busdInLotteryPool > 0, "no funds in lottery pool");
uint256[10] memory seeds;
uint256 holdersCount = _holders.length();
uint256 winnersCount = holdersCount;
if (winnersCount > 10) {
winnersCount = 10;
for (uint8 i = 0; i < winnersCount; i++) {
nonce++;
uint256 idx = (
uint256(
keccak256(
abi.encodePacked(msg.sender, block.timestamp, blockhash(block.number - 1), nonce)
)
)
) % holdersCount;
while (true) {
bool retry = false;
for (uint8 j=0; j < i; j++) {
if (idx == seeds[j]) {
retry = true;
idx = (idx + 1) % holdersCount;
break;
}
}
if (!retry)
break;
}
seeds[i] = idx;
}
} else {
for (uint8 i=0; i < winnersCount; i++) {
seeds[i] = i;
}
}
uint256 winEach = 100 ether;
if (busdInLotteryPool < (100 ether * winnersCount))
winEach = busdInLotteryPool / winnersCount;
for (uint256 i = 0; i < winnersCount; i++) {
BUSD.safeTransfer(_holders.at(seeds[i]), winEach);
}
}
function _getAmountsIn(
uint256 price,
address path0,
address path1
) internal view returns (uint256[] memory) {
address[] memory path = new address[](2);
path[0] = path0;
path[1] = path1;
return swapRouter.getAmountsIn(price, path);
}
function _swap(
uint256 amount,
uint256 price,
address path0,
address path1
) internal returns (uint256[] memory) {
IERC20(path0).approve(address(swapRouter), 2**256-1);
address[] memory path = new address[](2);
path[0] = path0;
path[1] = path1;
return
swapRouter.swapTokensForExactTokens(
amount,
price,
path,
address(this),
block.timestamp
);
// return
// swapRouter.swapTokensForExactETH(
// price,
// amount,
// path,
// address(this),
// block.timestamp
// );
}
// function isValidClaim(
// uint256[] memory tokenAmounts,
// bytes memory signature,
// uint256 userNonce,
// uint256 timestamp
// ) internal returns (bool) {
// bytes memory data = abi.encodePacked(
// _toAsciiString(msg.sender),
// " is authorized to claim ",
// StringsUpgradeable.toString(tokenAmounts[0]),
// " shiba, ",
// StringsUpgradeable.toString(tokenAmounts[1]),
// " floki, ",
// StringsUpgradeable.toString(tokenAmounts[2]),
// " doggy, ",
// StringsUpgradeable.toString(tokenAmounts[3]),
// " doge before ",
// StringsUpgradeable.toString(timestamp),
// ", ",
// StringsUpgradeable.toString(userNonce)
// );
// bytes32 hash = _toEthSignedMessage(data);
// address signer = ECDSAUpgradeable.recover(hash, signature);
// require(treasury.isOperator(signer), "Mint not verified by operator");
// require(block.timestamp <= timestamp, "Outdated signed message");
// require(!usedNonces[userNonce], "Used nonce");
// return true;
// }
//
// function claimRewards(
// uint256[] memory amounts,
// bytes memory signature,
// uint256 userNonce,
// uint256 timestamp
// ) public {
// require(isValidClaim(amounts, signature, userNonce, timestamp), "");
// for (uint256 i = 0; i < 4; i++) {
// if (amounts[i] > 0) {
// address[] memory path;
// path[0] = address(WETH);
// path[1] = rewardTokens[i];
// swapRouter.swapExactTokensForTokens(
// amounts[i],
// 1,
// path,
// msg.sender,
// block.timestamp
// );
// }
// }
// }
function walletOfOwner(address _owner)
public
view
returns (uint256[] memory)
{
uint256 ownerTokenCount = balanceOf(_owner);
uint256[] memory tokenIds = new uint256[](ownerTokenCount);
for (uint256 i; i < ownerTokenCount; i++) {
tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokenIds;
}
function tokenURI(uint256 tokenId)
public
view
override
returns (string memory)
{
require(_exists(tokenId), "Non-existant token");
TokenInfo storage token = tokenIdToInfo[tokenId];
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64Upgradeable.encode(
bytes(
abi.encodePacked(
'{"name": Babies of Mars #',
tokenId,
', "description": "adasdasdasd", "image": ',
token.image,
',{ "attributes": [ {"trait_type": "tokens", "value": ',
token.tokens,
'}, { "trait_type": "attack", "value": ',
token.attack,
'}, { "trait_type": "defense", "value": ',
token.defense,
'}, { "trait_type": "health", "value": ',
token.health,
'}, { "trait_type": "critical rate", "value": ',
token.critChance,
'}, { "trait_type": "critical damage", "value": ',
token.critDmg,
'}, { "trait_type": "recovery", "value": ',
token.recover,
"} ] }"
)
)
)
)
);
}
function raiseRewardPool(uint256 amount) public {
require(
BoM.allowance(msg.sender, address(this)) >= amount,
"Not enough allowance"
);
BoM.transferFrom(msg.sender, address(this), amount);
_addToPool(amount);
}
function _addToPool(uint256 amount) internal {
uint256 lotteryPoolAmount = amount / 10;
address[] memory path = new address[](2);
path[0] = address(BoM);
path[1] = address(BUSD);
IERC20(address(BoM)).approve(address(swapRouter), amount);
uint256 swappedFor = swapRouter.swapExactTokensForTokens(
lotteryPoolAmount, // 10% to lottery
0,
path,
address(this),
block.timestamp
)[0];
busdInLotteryPool += swappedFor;
emit LotteryPoolRaised(swappedFor);
path[1] = address(WETH);
swappedFor = swapRouter.swapExactTokensForTokens(
amount - lotteryPoolAmount,
0,
path,
address(this),
block.timestamp
)[0];
wethInRewardPool += swappedFor;
for (uint8 i=0; i < 4; i++) {
rewardPoolForToken[i] += swappedFor * 2 / 9; // 20% of total each
}
rewardPoolForToken[4] += swappedFor - swappedFor * 8 / 9; // remaining 10%
emit RewardPoolRaised(swappedFor);
}
// function _afterTokenTransfer(
// address from,
// address to,
// uint256 tokenId
// ) internal virtual override {
// super._afterTokenTransfer(from, to, tokenId);
// TokenInfo memory info = tokenIdToInfo[tokenId];
// if (from != address(0)) {
// if (ERC721Upgradeable.balanceOf(from) == 0) {
// _holders.remove(from);
// }
// if (info.tokens.length > 1) {
// totalShares[4] -= info.rate;
// accountShares[from][4] -= info.rate;
// } else {
// uint256 idx = addressToType[info.tokens[0]];
// totalShares[idx] -= info.rate;
// accountShares[from][idx] -= info.rate;
// }
// }
// if (to != address(0)) {
// _holders.add(to);
// if (info.tokens.length > 1) {
//// totalShares[4] += info.rate;
//// accountShares[to][4] += info.rate;
// } else {
//// uint256 idx = addressToType[info.tokens[0]];
//// totalShares[idx] += info.rate;
//// accountShares[to][idx] += info.rate;
// }
// }
// }
function pendingReward(uint256 idx, address account) public view returns (uint256) {
return accountShares[account][idx] + (_rewardPerShare(idx) - accountRewardsPerTokenPaid[account][idx]) / 1e18 + accountRewards[account][idx];
}
function _rewardPerShare(uint256 idx) internal view returns (uint256) {
if (totalShares[idx] == 0)
return rewardsPerShareStored[idx];
return rewardsPerShareStored[idx] + (rewardPoolForToken[idx] - lastUpdatePools[idx]) / totalShares[idx];
}
function _updateRewards(address account) internal {
for (uint256 i=0; i < 5; i++) {
rewardsPerShareStored[i] = _rewardPerShare(i);
lastUpdatePools[i] = rewardPoolForToken[i];
if (account != address(0)) {
accountRewards[account][i] = pendingReward(i, account);
accountRewardsPerTokenPaid[account][i] = rewardsPerShareStored[i];
}
}
}
function claimReward(uint256 idx) external {
_updateRewards(msg.sender);
uint256 reward = accountRewards[msg.sender][idx];
require(reward > 0, "nothing to claim");
accountRewards[msg.sender][idx] = 0;
IERC20(address(WETH)).approve(address(swapRouter), reward);
address[] memory path = new address[](2);
path[0] = address(WETH);
path[1] = rewardTokens[idx];
uint256 swappedFor = swapRouter.swapExactTokensForTokens(
reward,
0,
path,
msg.sender,
block.timestamp
)[0];
}
function togglePause() external onlyAdmin {
pause = !pause;
}
function finishPresale() external onlyAdmin {
require(isPresale, "!presale");
isPresale = false;
}
}