bom-contracts/contracts/nft.sol
2022-10-19 13:05:23 +03:00

638 lines
21 KiB
Solidity

// 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";
import "./interfaces/INFT.sol";
import "./interfaces/INFTMetadata.sol";
contract NFT is ERC721EnumerableUpgradeable, SignatureControl, INFT {
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,
.0008 ether,
.0006 ether,
.0004 ether
// 300 * 10**18,
// 250 * 10**18,
// 200 * 10**18,
// 250 * 10**18
];
INFTMetadata metadata;
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;
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;
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, "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
);
}
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-existent token");
TokenInfo memory token = tokenIdToInfo[tokenId];
return metadata.tokenURI(tokenId, token);
}
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 (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;
}
function updateMetadata(INFTMetadata newValue) external onlyAdmin {
metadata = newValue;
}
}