// 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 "./interfaces/ITreasury.sol"; import "./interfaces/IPancake.sol"; import "./mixins/signature-control.sol"; contract NFT is ERC721EnumerableUpgradeable, SignatureControl { using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; using SafeERC20Upgradeable for IERC20Upgradeable; //variables bool pause = true; 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 = [ 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, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000 ]; 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)); _; } modifier isUnpaused() { require(!pause); _; } modifier isValidWhitelistMint(uint256 amount, uint256 tokenType) { require(isPresale); require(isWhitelisted[msg.sender]); require( mintedOnWhitelist[msg.sender] + amount <= maxMintPerAddressDuringWhitelist ); require(tokenType <= 3); require( whitelistMintCapOfToken[tokenType] + amount <= whitelistMintCapPerType ); _; } modifier isValidMint(uint256 amount, uint256 tokenType) { require(!isPresale); require(tokenType <= 3); require(mintCapOfToken[tokenType] + amount <= nftCaps[tokenType]); _; } modifier isValidCombine(uint256[] memory tokenIds) { require(tokenIds.length > 1 && tokenIds.length <= 4); bool hasDupes; for (uint256 i = 0; i < tokenIds.length; i++) { require(ownerOf(tokenIds[i]) == msg.sender); require(tokenIdToType[tokenIds[i]].length == 1); for (uint256 index = i; index < tokenIds.length; index++) { if ( index != i && tokenIdToType[tokenIds[i]][0] == tokenIdToType[tokenIds[index]][0] ) { hasDupes = true; } } require(!hasDupes); } _; } 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++; _mint(msg.sender, supply); tokenIdToType[supply].push(rewardTokens[tokenType]); uint256 rate = createTokenStats(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++; _mint(msg.sender, supply); tokenIdToInfo[supply].tokens.push(rewardTokens[tokenType]); uint256 rate = createTokenStats(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); supply++; uint256 price; _mint(msg.sender, supply); address[] memory tokens = new address[](tokenIds.length); for (uint256 i = 0; i < tokenIds.length; i++) { TokenInfo storage token = tokenIdToInfo[tokenIds[i]]; address intermediateStorageForToken = tokenIdToType[tokenIds[i]][0]; tokenIdToInfo[supply].tokens.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); 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; 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) { address[] memory path; 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; path[0] = address(BoM); path[1] = address(BUSD); 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[from][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; address[] memory path; path[0] = address(WETH); path[1] = rewardTokens[idx]; uint256 swappedFor = swapRouter.swapExactTokensForTokens( reward, 0, path, msg.sender, block.timestamp )[0]; } }