// SPDX-License-Identifier: MIT pragma solidity 0.8.17; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "./lib/AccessControl.sol"; import "./lib/ValhallaPool.sol"; import "./lib/ValhallaBlackList.sol"; import "./lib/NFTGetter.sol"; import "./GNET.sol"; import "./NFTFounder.sol"; enum Rank { NoRank, Common, Rare, SuperRare, Epic, Legend, SuperLegend } struct Account { bool isRegistered; bool isImported; Rank rank; address referrer; uint downlineCount; uint directDownlineCount; uint rankUpdatedAt; uint rankRewardClaimedAt; } struct RankDistribution { uint common; uint rare; uint superRare; uint epic; uint legend; uint superLegend; } contract Valhalla is ValhallaPool, ValhallaBlackList, Initializable, AccessControl, UUPSUpgradeable { // state definition mapping(address => Account) public accountMap; mapping(address => uint) public rewardMap; mapping(address => uint) public directCommonRankMap; mapping(address => uint) public directRareRankMap; mapping(address => uint) public directSuperRareRankMap; mapping(address => uint) public directEpicRankMap; mapping(address => uint) public directLegendRankMap; mapping(address => uint) public directSuperLegendRankMap; address public feeReceiverAddress; address public feeReceiverAddress2; address public reserveAddress; bool public isRankRewardClaimable; uint public rankRewardClaimableAt; uint public deployedAtBlock; // ipoPoolDistrbution + referrerPooDistribution = registrationFee uint private ipoPoolDistribution; uint private referrerPoolDistribution; RankDistribution public rankDistribution; NFTGetter public nftGetter; GNET public gnetERC20; ERC20 public usdtERC20; NFTFounder public nftFounder; // events event Registration(address indexed _from, address indexed referrer); event ClaimReward(address indexed _from, uint value); event ClaimRankReward(address indexed _from, uint value); event Blacklisted(address indexed _target); event RankUpgraded(address indexed _from, Rank rank); event RankRewardOpened(uint indexed _timestamp); event RankRewardClosed(uint indexed _timestamp); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( address _admin, address[] memory root_address, address _feeReceiverAddress, address _feeReceiverAddress2, address _reserveAddress, uint _ipoPoolDistribution, uint _referrerPoolDistribution ) public initializer { __AccessControl_init(); __UUPSUpgradeable_init(); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(UPGRADER_ROLE, msg.sender); _grantRole(DEFAULT_ADMIN_ROLE, _admin); deployedAtBlock = block.number; ipoPoolDistribution = _ipoPoolDistribution; referrerPoolDistribution = _referrerPoolDistribution; feeReceiverAddress = _feeReceiverAddress; feeReceiverAddress2 = _feeReceiverAddress2; reserveAddress = _reserveAddress; accountMap[_feeReceiverAddress2].isRegistered = true; accountMap[_feeReceiverAddress2].referrer = address(0); accountMap[_feeReceiverAddress].isRegistered = true; accountMap[_feeReceiverAddress].referrer = address(0); emit Registration(_feeReceiverAddress, address(0)); accountMap[_reserveAddress].isRegistered = true; accountMap[_reserveAddress].referrer = address(0); emit Registration(_feeReceiverAddress, address(0)); address _parent; for (uint i = 0; i < root_address.length; i++) { address _current = root_address[i]; accountMap[_current].isRegistered = true; accountMap[_current].downlineCount = root_address.length - i - 1; accountMap[_current].directDownlineCount = 1; accountMap[_current].rank = Rank.NoRank; accountMap[_current].referrer = _parent; emit Registration(_current, _parent); _parent = _current; } } function _authorizeUpgrade( address newImplementation ) internal override onlyUpgrader {} function _addDownlineAndAdjustRank(address _address) private { Account storage _account = accountMap[_address]; _account.downlineCount += 1; // if its already in max rank dont do anything // for the sake of gas optimization if (_account.rank == Rank.SuperLegend) return; if (_account.downlineCount >= 64000 && _account.rank == Rank.Legend) { uint totalEligibleDirectRank = directLegendRankMap[_address] + directSuperLegendRankMap[_address]; if (totalEligibleDirectRank >= 2) { _account.rank = Rank.SuperLegend; rankDistribution.legend -= 1; rankDistribution.superLegend += 1; directLegendRankMap[_account.referrer] -= 1; directSuperLegendRankMap[_account.referrer] += 1; } } else if ( _account.downlineCount >= 16000 && _account.rank == Rank.Epic ) { uint totalEligibleDirectRank = directEpicRankMap[_address] + directLegendRankMap[_address] + directSuperLegendRankMap[_address]; if (totalEligibleDirectRank >= 2) { _account.rank = Rank.Legend; rankDistribution.epic -= 1; rankDistribution.legend += 1; directEpicRankMap[_account.referrer] -= 1; directLegendRankMap[_account.referrer] += 1; } } else if ( _account.downlineCount >= 6400 && _account.rank == Rank.SuperRare ) { uint totalEligibleDirectRank = directSuperRareRankMap[_address] + directEpicRankMap[_address] + directLegendRankMap[_address] + directSuperLegendRankMap[_address]; if (totalEligibleDirectRank >= 2) { _account.rank = Rank.Epic; rankDistribution.superRare -= 1; rankDistribution.epic += 1; directSuperRareRankMap[_account.referrer] -= 1; directEpicRankMap[_account.referrer] += 1; } } else if ( _account.downlineCount >= 1600 && _account.rank == Rank.Rare ) { uint totalEligibleDirectRank = directRareRankMap[_address] + directSuperRareRankMap[_address] + directEpicRankMap[_address] + directLegendRankMap[_address] + directSuperLegendRankMap[_address]; if (totalEligibleDirectRank >= 2) { _account.rank = Rank.SuperRare; rankDistribution.rare -= 1; rankDistribution.superRare += 1; directRareRankMap[_account.referrer] -= 1; directSuperRareRankMap[_account.referrer] += 1; } } else if ( _account.downlineCount >= 400 && _account.rank == Rank.Common ) { uint totalEligibleDirectRank = directCommonRankMap[_address] + directRareRankMap[_address] + directSuperRareRankMap[_address] + directEpicRankMap[_address] + directLegendRankMap[_address] + directSuperLegendRankMap[_address]; if (totalEligibleDirectRank >= 2) { _account.rank = Rank.Rare; rankDistribution.common -= 1; rankDistribution.rare += 1; directCommonRankMap[_account.referrer] -= 1; directRareRankMap[_account.referrer] += 1; } } else if ( _account.downlineCount >= 100 && _account.rank == Rank.NoRank ) { _account.rank = Rank.Common; rankDistribution.common += 1; directCommonRankMap[_account.referrer] += 1; } else { return; } emit RankUpgraded(_address, _account.rank); } function _distributeRegistrationFeeToNthReferrer( uint value ) private returns (uint) { uint _reference = value; uint _valueLeft = value; Account storage currentAccount = accountMap[msg.sender]; // rules of how registration fee going to be distributed // into up to 15 upline in percentage // root 1 - 2 = 10%, 3 = 6%, 4 = 5%, etc. uint8[15] memory uplineRegistrationFeeDistributionPercentage = [ 30, 8, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]; for ( uint i = 0; i < uplineRegistrationFeeDistributionPercentage.length; i++ ) { address _parentAddress = currentAccount.referrer; // exit loop once it encounter zero address // because it's expected to be no other upline can be processed if (_parentAddress == address(0)) break; // ignore blacklisted upline if (blacklistedAddressMap[_parentAddress] == false) { _addDownlineAndAdjustRank(_parentAddress); uint _percentage = uplineRegistrationFeeDistributionPercentage[ i ]; uint _distribution_value = (_reference * _percentage) / 100; rewardMap[_parentAddress] += _distribution_value; _valueLeft -= _distribution_value; } currentAccount = accountMap[_parentAddress]; } return _valueLeft; } function setNFTFounderAddress(NFTFounder _nftFounder) public onlyAdmin { nftFounder = _nftFounder; } function setNftAddress(NFTGetter _nftGetter) public onlyAdmin { nftGetter = _nftGetter; } function setGntAddress(GNET _gnetERC20) public onlyAdmin { gnetERC20 = _gnetERC20; } function setUsdtAddress(ERC20 _usdtERC20) public onlyAdmin { usdtERC20 = _usdtERC20; } function getRegistrationFee() public view returns (uint) { return ipoPoolDistribution + referrerPoolDistribution; } function register(address referrer) public notInBlacklist { uint registrationFee = getRegistrationFee(); Account storage account = accountMap[msg.sender]; uint balance = usdtERC20.balanceOf(msg.sender); require(address(usdtERC20) != address(0), "admin didn't set token ERC"); require(balance >= registrationFee, "Unmatch Registration Fee"); require(account.isRegistered == false, "Address already registered"); require(referrer != address(0), "Invalid referrer"); require( accountMap[referrer].isRegistered, "Referrer should be registered" ); require( blacklistedAddressMap[referrer] == false, "Referrer blacklisted" ); require(!isRankRewardClaimable, "Registration closed"); account.referrer = referrer; account.isRegistered = true; account.rank = Rank.NoRank; account.directDownlineCount = 0; accountMap[referrer].directDownlineCount += 1; _storeIpoPool(ipoPoolDistribution); uint valueLeft = _distributeRegistrationFeeToNthReferrer( referrerPoolDistribution ); uint globalPoolDistribution = (referrerPoolDistribution * 17) / 100; _storeGlobalPool(globalPoolDistribution); valueLeft -= globalPoolDistribution; uint reserveDistribution = (referrerPoolDistribution * 3) / 100; // need approval usdtERC20.transferFrom(msg.sender, address(this), registrationFee); usdtERC20.transfer(address(nftFounder), reserveDistribution); nftFounder.distributeFounderRewards(reserveDistribution); valueLeft -= reserveDistribution; usdtERC20.transfer(feeReceiverAddress, valueLeft / 2); usdtERC20.transfer(feeReceiverAddress2, valueLeft / 2); emit Registration(msg.sender, referrer); } function importAccount( address _address, address _referrer ) public onlyAdmin { Account storage account = accountMap[_address]; require(account.isRegistered == false, "Address already registered"); require(_referrer != address(0), "Invalid referrer"); require( accountMap[_referrer].isRegistered, "Referrer should be registered" ); require( blacklistedAddressMap[_referrer] == false, "Referrer blacklisted" ); require(!isRankRewardClaimable, "Registration closed"); account.referrer = _referrer; account.isRegistered = true; account.rank = Rank.NoRank; account.directDownlineCount = 0; account.isImported = true; accountMap[_referrer].directDownlineCount += 1; address _currentParent = _referrer; for (uint i = 0; i < 15; i++) { _addDownlineAndAdjustRank(_currentParent); _currentParent = accountMap[_currentParent].referrer; } emit Registration(_address, _referrer); } function claimReward() public notInBlacklist { uint _reward = rewardMap[msg.sender]; require(_reward > 0, "No reward to be claimed"); // apply 10% tax uint tax = (_reward * 10) / 100; // claim reward uint halfTax = tax / 2; usdtERC20.transfer(feeReceiverAddress, halfTax); usdtERC20.transfer(feeReceiverAddress2, halfTax); usdtERC20.transfer(msg.sender, _reward - tax); rewardMap[msg.sender] = 0; emit ClaimReward(msg.sender, _reward - tax); } function getMyRankReward(address _address) public view returns (uint) { Account memory account = accountMap[_address]; PoolType storage globalPool = poolMap[GLOBAL_POOL_KEY]; if (globalPool.valueLeft == 0) return 0; if (isRankRewardClaimable == false) return 0; if (account.rankRewardClaimedAt >= rankRewardClaimableAt) return 0; if (account.rank == Rank.NoRank) return 0; uint distribution = 0; uint base = 0; if (account.rank == Rank.Common) { distribution = rankDistribution.common; base = (globalPool.valueLeft * 3) / 100; } if (account.rank == Rank.Rare) { distribution = rankDistribution.rare; base = (globalPool.valueLeft * 7) / 100; } if (account.rank == Rank.SuperRare) { distribution = rankDistribution.superRare; base = (globalPool.valueLeft * 12) / 100; } if (account.rank == Rank.Epic) { distribution = rankDistribution.epic; base = (globalPool.valueLeft * 18) / 100; } if (account.rank == Rank.Legend) { distribution = rankDistribution.legend; base = (globalPool.valueLeft * 26) / 100; } if (account.rank == Rank.SuperLegend) { distribution = rankDistribution.superLegend; base = (globalPool.valueLeft * 34) / 100; } return base / distribution; } function claimRankReward() public notInBlacklist { uint myReward = getMyRankReward(msg.sender); uint nftValue = nftGetter.totalValueMap(msg.sender); Account storage account = accountMap[msg.sender]; require(myReward > 0, "No Reward can be claimed"); if (account.rank == Rank.Common) { require( nftValue >= 50_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } if (account.rank == Rank.Rare) { require( nftValue >= 200_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } if (account.rank == Rank.SuperRare) { require( nftValue >= 1000_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } if (account.rank == Rank.Epic) { require( nftValue >= 5_000_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } if (account.rank == Rank.Legend) { require( nftValue >= 25_000_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } if (account.rank == Rank.SuperLegend) { require( nftValue >= 100_000_000 * 10 ** gnetERC20.decimals(), "Not Eligible" ); } uint tax = (myReward * 10) / 100; uint halfTax = tax / 2; // claim rank reward usdtERC20.transfer(feeReceiverAddress, halfTax); usdtERC20.transfer(feeReceiverAddress2, halfTax); usdtERC20.transfer(msg.sender, myReward - tax); account.rankRewardClaimedAt = block.timestamp; PoolType storage globalPool = poolMap[GLOBAL_POOL_KEY]; globalPool.valueLeft -= myReward; emit ClaimRankReward(msg.sender, myReward); } function startClaimingRankReward() public onlyStaff { require(isRankRewardClaimable == false, "Already started"); isRankRewardClaimable = true; rankRewardClaimableAt = block.timestamp; PoolType storage globalPool = poolMap[GLOBAL_POOL_KEY]; globalPool.valueLeft = globalPool.claimable; globalPool.claimable = globalPool.claimable - globalPool.valueLeft; nftGetter.startClaimingRankReward(); emit RankRewardOpened(block.timestamp); } function stopClaimingRankReward() public onlyStaff { require(isRankRewardClaimable, "Rank reward not started"); isRankRewardClaimable = false; PoolType storage globalPool = poolMap[GLOBAL_POOL_KEY]; globalPool.claimable += globalPool.valueLeft; globalPool.valueLeft = 0; nftGetter.stopClaimingRankReward(); emit RankRewardClosed(block.timestamp); } function blackListAddress(address addr) public onlyAdmin { require(addr != msg.sender, "You cannot blacklist yourself"); require(addr != address(0), "You cannot blacklist address zero"); addToBlacklistMap(addr); emit Blacklisted(addr); } function wdUSDT(uint amount) public onlyAdmin { require(amount <= usdtERC20.balanceOf(address(this)), "Insufficient USDT balance"); usdtERC20.transfer(msg.sender, amount); } function changeFeeRegister( uint _ipoPol, uint _referalPol ) public onlyAdmin { ipoPoolDistribution = _ipoPol; referrerPoolDistribution = _referalPol; } }