// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "./Valhalla.sol"; import "./BullRonToken.sol"; struct NFTDetail { uint price; } struct Profile { uint value; uint buy_reward; uint claim_at; } contract BullRunV2 is Initializable, ERC721Upgradeable, ERC721EnumerableUpgradeable, ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, OwnableUpgradeable { uint256 private _nextTokenId; uint8 public total_list; uint256 public total_coin; uint public global_pool; IUniswapV2Router01 public swapRouter; IERC20 public usdt; Valhalla public valhalla; BullRunToken public bull_token; address public reserve1; address public reserve2; uint[] private _bonusPercent; bool public isClaimableProfit; bool public isRankRewardClaimAble; uint public startRankAt; uint public startProfitAt; mapping(uint => mapping(address => uint)) public nft_assets; mapping(uint => uint) public nft_to_list_id; mapping(address => mapping(address => address[][])) public swap_paths; mapping(uint => NFTDetail) public nft_list; mapping(uint => address) public coin_list; mapping(address => Profile) public profile; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( IUniswapV2Router01 _swapRouter, IERC20 _usdt, Valhalla _valhalla, address _reserve1, address _reserve2 ) public initializer { __ERC721_init("BullRun", "BLRN"); __ERC721Enumerable_init(); __ERC721URIStorage_init(); __ERC721Burnable_init(); __Ownable_init(); swapRouter = _swapRouter; usdt = _usdt; valhalla = _valhalla; reserve1 = _reserve1; reserve2 = _reserve2; _bonusPercent = [10, 7, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1]; } function _baseURI() internal pure override returns (string memory) { return "https://globalnetwork.finance/api/bull-run/"; } function safeMint(address to, string memory uri) private { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); } // find the best path based on path list // that stored on state variable swap_paths function get_swap_path( address _addressIn, address _addressOut, uint _amountIn ) public view returns (address[] memory) { address[][] memory path_list = swap_paths[_addressIn][_addressOut]; address[] memory best_path = new address[](2); best_path[0] = _addressIn; best_path[1] = _addressOut; // initialize using direct path uint best_output = swapRouter.getAmountsOut(_amountIn, best_path)[1]; for (uint i = 0; i < path_list.length; i++) { address[] memory path_i = path_list[i]; uint[] memory output_i = swapRouter.getAmountsOut( _amountIn, path_i ); if (output_i[output_i.length - 1] > best_output) { best_output = output_i[output_i.length - 1]; best_path = path_i; } } return best_path; } function get_paths( address address_in, address address_out ) public view returns (address[][] memory path_list) { return swap_paths[address_in][address_out]; } function set_paths( address address_in, address address_out, address[] memory paths ) public onlyOwner returns (bool) { address[][] memory path_list = swap_paths[address_in][address_out]; for (uint i = 0; i < path_list.length; i++) { if (path_list[i].length == paths.length) { bool is_same = true; for (uint j = 0; j < paths.length; j++) { if (path_list[i][j] != paths[j]) { is_same = false; break; } } if (is_same) { return false; } } } swap_paths[address_in][address_out].push(paths); return true; } // The following functions are overrides required by Solidity. function tokenURI( uint256 tokenId ) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) returns (string memory) { return super.tokenURI(tokenId); } function _burn( uint256 tokenId ) internal virtual override(ERC721URIStorageUpgradeable, ERC721Upgradeable) { super._burn(tokenId); } function _beforeTokenTransfer( address from, address to, uint256 firstTokenId, uint256 batchSize ) internal virtual override(ERC721Upgradeable, ERC721EnumerableUpgradeable) { uint listId = nft_to_list_id[firstTokenId]; uint currentValue = nft_list[listId].price; if (from != address(0)) { profile[from].value -= currentValue; } if (to != address(0)) { profile[to].value += currentValue; } super._beforeTokenTransfer(from, to, firstTokenId, batchSize); } function supportsInterface( bytes4 interfaceId ) public view override( ERC721Upgradeable, ERC721EnumerableUpgradeable, ERC721URIStorageUpgradeable ) returns (bool) { return super.supportsInterface(interfaceId); } function getMyRankReward(address _profile) public view returns (uint) { uint common; uint rare; uint superRare; uint epic; uint legend; uint superLegend; (common, rare, superRare, epic, legend, superLegend) = valhalla .rankDistribution(); Rank _myRank; (, , _myRank, , , , , ) = valhalla.accountMap(_profile); uint rareUp = rare + superRare + epic + legend + superLegend; uint toCommon = (global_pool * 40) / 100; uint toRareUp = global_pool - toCommon; if (_myRank == Rank.Common) { if (common == 0) return 0; uint commonGet = toCommon / common; return commonGet; } else if (_myRank != Rank.NoRank) { if (rareUp == 0) return 0; uint rareUpGet = toRareUp / rareUp; return rareUpGet; } else { return 0; } } function addCoin(address _cointAddres) public onlyOwner { uint current = total_coin; coin_list[current] = _cointAddres; total_coin++; } function addNft(uint _price) public onlyOwner { uint current = total_list; nft_list[current].price = _price; total_list++; } function lastStartRankAt() public view returns (uint) { return startRankAt; } function lastStartProfieAt() public view returns (uint) { return startProfitAt; } function startStopProfit() public onlyOwner { startProfitAt = block.timestamp; isClaimableProfit = !isClaimableProfit; } function startStopRankReward() public onlyOwner { startRankAt = block.timestamp; isRankRewardClaimAble = !isRankRewardClaimAble; } function setBullToken(BullRunToken _bullTokenAddress) public onlyOwner { require(bull_token != _bullTokenAddress, "address already set"); bull_token = _bullTokenAddress; } function setFeeReserve( address _address1, address _address2 ) public onlyOwner { reserve1 = _address1; reserve2 = _address2; } // untuk memperbaiki uri yg salah function fixUri(uint tokenId, uint listId) public onlyOwner { _setTokenURI(tokenId, Strings.toString(listId)); } function getIsClaimRank() public view returns (bool) { return isRankRewardClaimAble; } function getIsClaimProfite() public view returns (bool) { return isClaimableProfit; } function getTotalCoin() public view returns (uint) { return total_coin; } function getTotalList() public view returns (uint) { return total_list; } function getGlobalPool() public view returns (uint) { return global_pool; } function getCoinById(uint listId) public view returns (address) { return coin_list[listId]; } function getDetailPreNft( uint listId ) public view returns (NFTDetail memory) { return nft_list[listId]; } function getProfileStatus( address _addressWallet ) public view returns (Profile memory) { return profile[_addressWallet]; } function getNftAsset( uint tokenId, address addressTokenAsset ) public view returns (uint) { return nft_assets[tokenId][addressTokenAsset]; } function getBonusPercent() public view returns (uint[] memory) { return _bonusPercent; } function getListId(uint tokenId) public view returns (uint) { return nft_to_list_id[tokenId]; } function swap( uint256 amountIn, address _addressIn, address _addressOut, address to ) public returns (uint[] memory amounts) { require( IERC20(_addressIn).approve(address(swapRouter), amountIn), "Approve failed." ); address[] memory path = get_swap_path( _addressIn, _addressOut, amountIn ); uint[] memory estimatedOut = swapRouter.getAmountsOut(amountIn, path); // slipage 7% uint slipage = (estimatedOut[path.length - 1] * 7) / 100; uint[] memory output_amounts = swapRouter.swapExactTokensForTokens( amountIn, estimatedOut[path.length - 1] - slipage, path, to, block.timestamp ); return output_amounts; } function _shareReward( address _user, uint _price ) private returns (uint, uint) { Rank rank; address referral; uint toGlobalPool = (_price * 20) / 100; uint toInvestment = (_price * 264) / 1000; // 26,4 percent uint toBullCoin = (_price * 136) / 1000; // 13.6 percent uint rest = _price; rest -= toInvestment; rest -= toBullCoin; rest -= toGlobalPool; global_pool += toGlobalPool; (, , rank, referral, , , , ) = valhalla.accountMap(_user); for (uint i; i < _bonusPercent.length; i++) { if (referral == address(0)) break; uint bonus = (_price * _bonusPercent[i]) / 100; Profile storage _profile = profile[referral]; if ( i > 7 && rank >= Rank.Rare && _profile.value >= 3000 * (10 ** 6) ) { _profile.buy_reward += bonus; rest -= bonus; } if ( i > 2 && i < 8 && rank == Rank.Common && _profile.value >= 1000 * (10 ** 6) ) { _profile.buy_reward += bonus; rest -= bonus; } if (i < 3 && _profile.value >= 100 * (10 ** 6)) { _profile.buy_reward += bonus; rest -= bonus; } // emit ShareToken(_user, referral, bonus, block.timestamp); (, , rank, referral, , , , ) = valhalla.accountMap(referral); } uint toReserve1 = rest / 2; profile[reserve1].buy_reward += toReserve1; rest -= toReserve1; profile[reserve2].buy_reward += rest; return (toBullCoin, toInvestment); } function buyNft(uint _listId) public onlyNetwork { uint price = nft_list[_listId].price; require( isRankRewardClaimAble == false, "Claim period active; no buying allowed." ); require(total_coin > 0, "coin not set yet"); require( usdt.transferFrom(msg.sender, address(this), price), "TransferFrom failed." ); uint256 tokenId = _nextTokenId++; nft_to_list_id[tokenId] = _listId; _safeMint(msg.sender, tokenId); _setTokenURI(tokenId, Strings.toString(_listId)); // usdt value after share to other network (uint usdt_to_bull_token, uint to_investment) = _shareReward( msg.sender, price ); bull_token.buyToken(usdt_to_bull_token); uint amountBull = bull_token.getValue(usdt_to_bull_token); nft_assets[tokenId][address(bull_token)] = amountBull; usdt.transfer(address(bull_token), usdt_to_bull_token); uint per_token_bought = to_investment / total_coin; for (uint i = 0; i < total_coin; i++) { uint[] memory amounts; amounts = swap( per_token_bought, address(usdt), coin_list[i], address(this) ); nft_assets[tokenId][coin_list[i]] = amounts[amounts.length - 1]; } } function claimBuyReward() public { uint getReward = profile[msg.sender].buy_reward; require(getReward > 0, "no reward left"); IERC20(usdt).transfer(msg.sender, getReward); profile[msg.sender].buy_reward = 0; } function claimRankReward() public isRankStarted { uint startAt = startRankAt; require( profile[msg.sender].claim_at < startAt, "You already claim Reward" ); uint myReward = getMyRankReward(msg.sender); Rank _myRank; (, , _myRank, , , , , ) = valhalla.accountMap(msg.sender); profile[msg.sender].claim_at = block.timestamp; require(_myRank != Rank.NoRank, "Boost Your Rank First"); if (_myRank == Rank.Common) { require( profile[msg.sender].value >= 1000 * 10 ** 6, "Require 1000 NFT Value To claim the Reward" ); usdt.transfer(msg.sender, myReward); } else if (_myRank != Rank.NoRank) { require( profile[msg.sender].value >= 1000 * 10 ** 6, "Require 3000 NFT Value To claim the Reward" ); usdt.transfer(msg.sender, myReward); } } function claimProfit(uint tokenId) public isStartProfit { require(ownerOf(tokenId) == msg.sender, "Not the owner"); uint bullTokenAmount = nft_assets[tokenId][address(bull_token)]; bull_token.transfer(msg.sender, bullTokenAmount); for (uint i = 0; i < total_coin; i++) { uint amount = nft_assets[tokenId][coin_list[i]]; if (amount > 0) { uint fee = (amount * 10) / 100; swap(amount - fee, coin_list[i], address(usdt), msg.sender); uint[] memory toReceiver = swap( fee, coin_list[i], address(usdt), address(this) ); uint usdtAmount = toReceiver[toReceiver.length - 1]; uint split = usdtAmount / 2; profile[reserve1].buy_reward = split; profile[reserve2].buy_reward = usdtAmount - split; delete nft_assets[tokenId][coin_list[i]]; } } _burn(tokenId); } // fix value of owner NFT function fixProfileValue( address _ownerNft, uint _actualValue ) public onlyOwner { profile[_ownerNft].value = _actualValue; } modifier isStartProfit() { require(isClaimableProfit, "claim profit didn't start yet"); _; } modifier isRankStarted() { require(isRankRewardClaimAble, "rank hasn't started"); _; } modifier onlyNetwork() { bool isReg; (isReg, , , , , , , ) = valhalla.accountMap(msg.sender); require(isReg, "Only Network Can buy Nft"); _; } }