// SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts ^5.0.0 pragma solidity ^0.8.20; 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-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; import "./Usdt.sol"; import "./Network.sol"; import "hardhat/console.sol"; struct InvestmentRecord { uint256 amount; address contractAddress; } contract Bullcuan is Initializable, ERC721Upgradeable, ERC721EnumerableUpgradeable, ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, OwnableUpgradeable { uint256 private _nextTokenId; /** * Stores the USDT token contract address, the Network contract address, the address of the BullRun manager, and flags related to the NFT minting process. * * @dev usdt - The USDT token contract address. * @dev network - The Network contract address. * @dev bullRunManager - The address of the BullRun manager. * @dev isStartedClaim - A flag indicating whether the NFT minting process has started. * @dev totalCoin - The total available coin. * @dev IUniswapV2Router01 - Interface for the Uniswap v2 router contract used for swapping tokens. * @dev listPreMinted - A mapping of list IDs to their corresponding USDT prices. * @dev tokenIdToCoinAmount - A mapping of token IDs to their corresponding investment records. * @dev coinAddress - A mapping of token IDs to their corresponding ERC20 token contract addresses. * @dev swap_paths - A mapping of token pairs to their Uniswap * @dev isClaimed - A mapping of token IDs to whether they have been claimed. * @dev tokenClaimAt - A mapping of token IDs to their corresponding claim dates. */ USDT public usdt; Network public network; address public bullRunManager; bool public isStartedClaim; uint public totalCoin; IUniswapV2Router01 public swapRouter; // listId to price mapping(uint256 => uint256) public listPreMinted; // tokenId to invesment token mapping(uint256 => mapping(address => InvestmentRecord)) public tokenIdToCoinAmount; // token erc20 address list mapping(uint256 => address) public coinAddress; // swap path mapping(address => mapping(address => address[][])) public swap_paths; // isClaimed tokenId mapping(uint256 => bool) public isClaimed; // isClaimed at mapping(uint256 => uint256) public tokenClaimAt; // tokenId to list id mapping(uint => uint) public tokenIdToListId; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /** * Initializes the NFT contract with the necessary dependencies and configurations. * @dev should change decimal to 18, in real apps. for now make the prices low just for testing * @dev should change name and symbol in real apps * * @param _usdt - The USDT token contract address. * @param _network - The Network contract address. * @param _bullrunManager - The address of the BullRun manager. * @param _tokenErc20 - An array of ERC20 token contract addresses. */ function initialize( USDT _usdt, Network _network, address _bullrunManager, IUniswapV2Router01 _router, address[] memory _tokenErc20 ) public initializer { __ERC721_init("Pasuruan Dev", "PSD"); __ERC721URIStorage_init(); __ERC721Burnable_init(); __Ownable_init(); usdt = _usdt; network = _network; bullRunManager = _bullrunManager; isStartedClaim = false; uint16[6] memory price = [15, 30, 100, 200, 500, 1000]; for (uint16 i = 0; i < price.length; i++) { listPreMinted[i] = price[i] * 10 ** usdt.decimals(); // should change to real decimals } for (uint256 i = 0; i < _tokenErc20.length; i++) { coinAddress[i] = _tokenErc20[i]; } totalCoin = _tokenErc20.length; swapRouter = _router; } function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal override(ERC721Upgradeable, ERC721EnumerableUpgradeable) { super._beforeTokenTransfer(from, to, tokenId, batchSize); } function _burn(uint256 tokenId) internal override(ERC721Upgradeable, ERC721URIStorageUpgradeable) { super._burn(tokenId); } // the uri shouldbe change before deployed function _baseURI() internal pure override returns (string memory) { return "https://bullcuan.com/api/nft/"; } function safeMint(address to, string memory uri) public onlyOwner { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); } // 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 supportsInterface( bytes4 interfaceId ) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable, ERC721EnumerableUpgradeable) returns (bool) { return super.supportsInterface(interfaceId); } /** * Shares the remaining USDT after deducting the owner's fee from the total amount. * * @param _price - The total price of the NFT. * @return The remaining USDT amount after deducting the referral, marketing, and BullRun manager rewards. * @dev This function calculates and transfers the referral, marketing, and BullRun manager rewards, and returns the remaining USDT amount. */ function shareReward(uint _price) private returns (uint) { uint256 rest = _price; uint256 referralReward = (_price * 10) / 100; uint256 marketingReward = (_price * 30) / 100; uint256 bullRunManagerReward = (_price * 30) / 100; address referralAddress = network.getReferral(msg.sender); usdt.transfer(referralAddress, referralReward); rest -= referralReward; usdt.transfer(bullRunManager, bullRunManagerReward); rest -= bullRunManagerReward; usdt.transfer(owner(), marketingReward); rest -= marketingReward; return rest; } /** * Shares the remaining USDT after deducting the owner's fee from the total amount. * * @param _tokenId - The token ID associated with the USDT amount. * @param _coinAddress - The contract address of the token. * @param _restUsdt - The remaining USDT amount after deducting the owner's fee. * @dev This function transfers 10% of the remaining USDT to the contract owner, and the rest to the user who initiated the transaction. */ function shareRewardClaim(uint _tokenId, address _coinAddress, uint _restUsdt) private { // need to share 10% to owner and the rest send to the user uint feeOwnerContract = (_restUsdt * 10) / 100; usdt.transfer(owner(), feeOwnerContract); _restUsdt -= feeOwnerContract; usdt.transfer(msg.sender, _restUsdt); // reset coint amount to zero after transfering result tokenIdToCoinAmount[_tokenId][_coinAddress].amount = 0; } /** * Stores the contract address and amount of a specific token ID. * * @param _tokenId - The token ID to associate the contract address and amount with. * @param _coinAddress - The contract address of the token. * @param _amount - The amount of the token. * @dev This function is used internally to store the contract address and amount for a given token ID. */ function setTokenId(uint _tokenId, address _coinAddress, uint _amount) private { // write in to tokenID tokenIdToCoinAmount[_tokenId][_coinAddress].contractAddress = _coinAddress; tokenIdToCoinAmount[_tokenId][_coinAddress].amount = _amount; } /** * Allows a user to buy an NFT from a pre-minted list. * * @param _listId - The ID of the pre-minted NFT to purchase. * @dev This function checks that the claim has not already started, retrieves the price of the NFT from the `listPreMinted` mapping, transfers the required USDT tokens from the user to the contract, calculates and distributes the referral, marketing, and BullRun manager rewards, and then mints the NFT to the user's address. */ function buyNft(uint256 _listId) public { require(!isStartedClaim, "Claim Already Started"); bool status = network.getStatus(msg.sender); // this require active if on real main require(status, "register first"); uint256 tokenId = _nextTokenId; uint256 price = listPreMinted[_listId]; require(price > 0, "List id not valid"); usdt.transferFrom(msg.sender, address(this), price); uint256 rest = shareReward(price); tokenIdToListId[tokenId] = _listId; trySwap(tokenId, rest); _safeMint(msg.sender, tokenId); _nextTokenId++; } /** * Allows a user to purchase an NFT by swapping tokens for USDT, with a custom swap route. * * @param _listId The ID of the NFT listing to purchase. * @param _path The custom swap route to use for the token swaps. * @dev Reverts if the claim process has already started, the user is not registered, or the listing ID is not valid. */ function buyNftCustomRoute(uint256 _listId, address[][] memory _path) public { require(!isStartedClaim, "Claim Already Started"); bool status = network.getStatus(msg.sender); // this require active if on real main require(status, "register first"); uint256 tokenId = _nextTokenId; uint256 price = listPreMinted[_listId]; require(price > 0, "List id not valid"); usdt.transferFrom(msg.sender, address(this), price); uint256 rest = shareReward(price); tokenIdToListId[tokenId] = _listId; trySwapCustom(tokenId, rest, _path); _safeMint(msg.sender, tokenId); _nextTokenId++; } // swap to usdt, sub 10% to owner // make sure to update new path (route path) /** * Claims an NFT token by swapping the token's associated coins for USDT, transferring 10% of the USDT to the contract owner, and the remaining USDT to the token owner. * * @param _tokenId The ID of the NFT token to claim. * @dev Reverts if the caller is not the owner of the token, the claim process has not started, or the token has already been claimed. */ function claimNft(uint256 _tokenId) public { require(ownerOf(_tokenId) == msg.sender, "Not owner of token"); require(isStartedClaim, "Claim not started yet"); require(!isClaimed[_tokenId], "token already claim"); for (uint i = 0; i < totalCoin; i++) { uint cointPerTokenId = tokenIdToCoinAmount[_tokenId][coinAddress[i]].amount; uint[] memory coinAmount = swap(cointPerTokenId, coinAddress[i], address(usdt), address(this)); uint restUsdt = coinAmount[coinAmount.length - 1]; shareRewardClaim(_tokenId, coinAddress[i], restUsdt); } isClaimed[_tokenId] = true; tokenClaimAt[_tokenId] = block.timestamp; } /** * Claims the NFT for the given token ID, swapping the token amounts for each token in the contract's token list using the specified swap paths. * * @param _tokenId The ID of the token to claim. * @param _path An array of token address paths to use for the swaps. */ function claimNftCustomRoute(uint256 _tokenId, address[][] memory _path) public { require(ownerOf(_tokenId) == msg.sender, "Not owner of token"); require(isStartedClaim, "Claim not started yet"); require(!isClaimed[_tokenId], "token already claim"); for (uint i = 0; i < totalCoin; i++) { uint cointPerTokenId = tokenIdToCoinAmount[_tokenId][coinAddress[i]].amount; uint[] memory coinAmount = swapCustomRoute(cointPerTokenId, coinAddress[i], address(this), _path[i]); uint restUsdt = coinAmount[coinAmount.length - 1]; shareRewardClaim(_tokenId, coinAddress[i], restUsdt); } isClaimed[_tokenId] = true; tokenClaimAt[_tokenId] = block.timestamp; } /** * Attempts to swap a given amount of tokens for each token in the contract's token list. * * @param _tokenId The ID of the token to perform the swaps for. * @param _value The total amount of tokens to swap. */ function trySwap(uint256 _tokenId, uint256 _value) public { uint cointPerTokenId = _value / totalCoin; for (uint i = 0; i < totalCoin; i++) { uint[] memory coinAmount = swap(cointPerTokenId, address(usdt), coinAddress[i], address(this)); setTokenId(_tokenId, coinAddress[i], coinAmount[coinAmount.length - 1]); } } /** * Swaps a custom amount of tokens for each token ID, using a specified swap path. * * @param _tokenId The token ID to swap tokens for. * @param _value The total value of tokens to swap. * @param _path An array of token address paths to use for the swaps. */ function trySwapCustom(uint256 _tokenId, uint256 _value, address[][] memory _path) public { uint cointPerTokenId = _value / totalCoin; for (uint i = 0; i < totalCoin; i++) { uint[] memory coinAmount = swapCustomRoute(cointPerTokenId, address(usdt), address(this), _path[i]); setTokenId(_tokenId, coinAddress[i], coinAmount[coinAmount.length - 1]); } } function getCoinAmount(uint256 _tokenId, address _contractAddress) public view returns (uint) { return tokenIdToCoinAmount[_tokenId][_contractAddress].amount; } /** * Gets the investment details for a given token ID. * * @param _tokenId The token ID to get the investment details for. * @return allRecodByTokenId An array of `InvestmentRecord` structs containing the investment details for the given token ID. */ function getCoinInvestDetail(uint _tokenId) public view returns (InvestmentRecord[] memory) { InvestmentRecord[] memory allRecodByTokenId = new InvestmentRecord[](totalCoin); for (uint i = 0; i < totalCoin; i++) { InvestmentRecord memory record = InvestmentRecord({ contractAddress: coinAddress[i], amount: tokenIdToCoinAmount[_tokenId][coinAddress[i]].amount }); allRecodByTokenId[i] = record; } return allRecodByTokenId; } /** * Toggles the state of the claim process. * This function can only be called by the contract owner. */ function startStopClaim() public onlyOwner { isStartedClaim = !isStartedClaim; } /** * @dev Swaps a given amount of tokens from one address to another, using the best available swap path. * * @param amountIn The amount of tokens to swap. * @param _addressIn The address of the token to swap from. * @param _addressOut The address of the token to swap to. * @param to The address to receive the swapped tokens. * @return amounts The amounts of tokens swapped. */ 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 = getBestSwapPath(_addressIn, _addressOut, amountIn); uint[] memory estimatedOut = swapRouter.getAmountsOut(amountIn, path); // slipage 5% uint slipage = (estimatedOut[path.length - 1] * 5) / 100; uint[] memory output_amounts = swapRouter.swapExactTokensForTokens(amountIn, estimatedOut[path.length - 1] - slipage, path, to, block.timestamp); return output_amounts; } /** * Swaps tokens using a custom route path. * * @param amountIn The amount of tokens to swap. * @param _addressIn The address of the token to swap from. * @param _to The address to receive the swapped tokens. * @param _customRoutePath The custom route path to use for the swap. * @return amounts The amounts of tokens swapped. */ function swapCustomRoute( uint256 amountIn, address _addressIn, address _to, address[] memory _customRoutePath ) public returns (uint[] memory amounts) { require(IERC20(_addressIn).approve(address(swapRouter), amountIn), "Approve failed."); uint[] memory estimatedOut = swapRouter.getAmountsOut(amountIn, _customRoutePath); // slipage 5% uint slipage = (estimatedOut[_customRoutePath.length - 1] * 5) / 100; uint[] memory output_amounts = swapRouter.swapExactTokensForTokens( amountIn, estimatedOut[_customRoutePath.length - 1] - slipage, _customRoutePath, _to, block.timestamp ); return output_amounts; } /** * Retrieves the optimal swap path between two addresses, given an input amount. * * @param _addressIn The address to swap from. * @param _addressOut The address to swap to. * @param _amountIn The input amount to swap. * @return The optimal swap path between the two addresses. */ function getBestSwapPath(address _addressIn, address _addressOut, uint _amountIn) public view returns (address[] memory) { address[][] memory path_list = swap_paths[_addressIn][_addressOut]; address[] memory bestPath = new address[](2); bestPath[0] = _addressIn; bestPath[1] = _addressOut; // initialize using direct path uint best_output = swapRouter.getAmountsOut(_amountIn, bestPath)[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]; bestPath = path_i; } } return bestPath; } /** * Sets the swap paths for a given input and output token addresses. * * @param address_in The input token address. * @param address_out The output token address. * @param paths The array of token addresses representing the swap path. * @return true if the path was successfully added, false if it already exists. */ function setCoinPaths(address address_in, address address_out, address[] memory paths) public onlyOwner returns (bool) { address[][] memory pathList = swap_paths[address_in][address_out]; for (uint i = 0; i < pathList.length; i++) { if (pathList[i].length == paths.length) { bool is_same = true; for (uint j = 0; j < paths.length; j++) { if (pathList[i][j] != paths[j]) { is_same = false; break; } } if (is_same) { return false; } } } swap_paths[address_in][address_out].push(paths); return true; } function getCoinPath(address _addressIn, address _addressOut) public view returns (address[][] memory) { return swap_paths[_addressIn][_addressOut]; } function getLisCoin() public view returns (address[] memory) { address[] memory list = new address[](totalCoin); for (uint i = 0; i < totalCoin; i++) { list[i] = coinAddress[i]; } return list; } function changePrice(uint _listId, uint _price) public onlyOwner { listPreMinted[_listId] = _price; } function changeSwapRouter(IUniswapV2Router01 _addressSwap) public onlyOwner { swapRouter = _addressSwap; } function getTypeFromTokenId(uint _tokenID) public view returns (uint) { return tokenIdToListId[_tokenID]; } }