diff --git a/contracts/cryptofighters/CryptoFightersV2.sol b/contracts/cryptofighters/CryptoFightersV2.sol index ed1d196..1f9f73c 100644 --- a/contracts/cryptofighters/CryptoFightersV2.sol +++ b/contracts/cryptofighters/CryptoFightersV2.sol @@ -2,34 +2,50 @@ pragma solidity ^0.8.0; import "erc721a/contracts/ERC721A.sol"; +import "./IERC721R.sol"; import "./CryptoFightersPotion.sol"; +import "@openzeppelin/contracts/token/common/ERC2981.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -contract CryptoFightersV2 is ERC721A, Ownable, ReentrancyGuard { - uint256 public maxMintSupply = 8000; - uint256 public constant mintPrice = 0.1 ether; - uint256 public constant mintPriceWithPotion = 0.05 ether; +contract CryptoFightersV2 is + IERC721R, + ERC721A, + ERC2981, + Ownable, + ReentrancyGuard +{ + uint256 public maxMintSupply = 8000; // Max mintable supply from presale, public sale, and owner + uint256 public constant mintPrice = 0.08 ether; // Mint price for presale and public sale + uint256 public constant mintPriceWithPotion = 0.04 ether; // Mint price for upgrading v1 fighter with potion + uint256 public maxUserMintAmount = 5; // Max mintable amount per user, includes presale and public sale // Sale Status bool public publicSaleActive; bool public presaleActive; - uint256 public amountMinted; - uint256 public refundEndTime; - address public refundAddress; - uint256 public maxUserMintAmount; - mapping(address => uint256) public userMintedAmount; - bytes32 public merkleRoot; + uint256 public refundEndTime; // Time from which refunds will no longer be valid + address public refundAddress; // Address which refunded NFTs will be sent to - mapping(uint256 => bool) public hasV1FighterBeenUpgraded; - mapping(uint256 => uint256) public v2ToV1Mapping; + bytes32 public merkleRoot; // Merkle root for presale participants + + mapping(uint256 => bool) public hasRefunded; // users can search if the NFT has been refunded + mapping(uint256 => bool) public hasRevokedRefund; // users can revoke refund capability for e.g staking, airdrops + mapping(uint256 => bool) public isOwnerMint; // if the NFT was freely minted by owner + + mapping(uint256 => bool) public hasV1FighterBeenUpgraded; // mapping storing v1 fighters that have been upgraded + mapping(uint256 => uint256) public v2ToV1Mapping; // mapping connecting v2 fighters to v1 fighters string private baseURI; IERC721 private immutable cryptoFightersV1; CryptoFightersPotion private immutable cryptoFightersPotion; + modifier notContract() { + require(!Address.isContract(msg.sender), "No contracts"); + _; + } + constructor(address _cryptoFightersV1, address _cryptoFightersPotion) ERC721A("CryptoFightersAlliance", "CFA") { @@ -62,22 +78,18 @@ contract CryptoFightersV2 is ERC721A, Ownable, ReentrancyGuard { function mintV2FightersPresale(uint256 quantity, bytes32[] calldata proof) external payable - nonReentrant { require(presaleActive, "Presale is not active"); require(msg.value == quantity * mintPrice, "Value"); require( _isAllowlisted(msg.sender, proof, merkleRoot), - "Not whitelisted" + "Not allowlisted" ); require( - userMintedAmount[msg.sender] + quantity <= maxUserMintAmount, + _numberMinted(msg.sender) + quantity <= maxUserMintAmount, "Max amount" ); - require(amountMinted + quantity <= maxMintSupply, "Max mint supply"); - - amountMinted += quantity; - userMintedAmount[msg.sender] += quantity; + require(_totalMinted() + quantity <= maxMintSupply, "Max mint supply"); _safeMint(msg.sender, quantity); } @@ -85,24 +97,25 @@ contract CryptoFightersV2 is ERC721A, Ownable, ReentrancyGuard { function mintV2FightersPublicSale(uint256 quantity) external payable - nonReentrant + notContract { require(publicSaleActive, "Public sale is not active"); require(msg.value == quantity * mintPrice, "Value"); require( - userMintedAmount[msg.sender] + quantity <= maxUserMintAmount, + _numberMinted(msg.sender) + quantity <= maxUserMintAmount, "Max amount" ); - require(amountMinted + quantity <= maxMintSupply, "Max mint supply"); + require(_totalMinted() + quantity <= maxMintSupply, "Max mint supply"); - amountMinted += quantity; - userMintedAmount[msg.sender] += quantity; _safeMint(msg.sender, quantity); } - function ownerMint(uint256 quantity) external onlyOwner nonReentrant { - require(amountMinted + quantity <= maxMintSupply, "Max mint supply"); - _safeMint(msg.sender, quantity); + function ownerMint(uint256 quantity, address to) external onlyOwner { + require(_totalMinted() + quantity <= maxMintSupply, "Max mint supply"); + _safeMint(to, quantity); + for (uint256 i = _currentIndex - quantity; i < _currentIndex; i++) { + isOwnerMint[i] = true; + } } function refund(uint256[] calldata tokenIds) external nonReentrant { @@ -111,31 +124,92 @@ contract CryptoFightersV2 is ERC721A, Ownable, ReentrancyGuard { for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; require(msg.sender == ownerOf(tokenId), "Not owner"); + require(!hasRefunded[tokenId], "Already refunded"); + require( + !isOwnerMint[tokenId], + "Freely minted NFTs cannot be refunded" + ); + hasRefunded[tokenId] = true; transferFrom(msg.sender, refundAddress, tokenId); - if (v2ToV1Mapping[tokenId] != 0) { - refundAmount += mintPriceWithPotion; - } else { - refundAmount += mintPrice; - } + uint256 tokenAmount = v2ToV1Mapping[tokenId] == 0 + ? mintPrice + : mintPriceWithPotion; + + refundAmount += tokenAmount; + emit Refund(msg.sender, tokenId, tokenAmount); } Address.sendValue(payable(msg.sender), refundAmount); } + function revokeRefund(uint256[] calldata tokenIds) external { + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 tokenId = tokenIds[i]; + require(msg.sender == ownerOf(tokenId), "Not owner"); + hasRevokedRefund[tokenId] = true; + } + } + + function getRefundPrice(uint256 tokenId) public view returns (uint256) { + if (v2ToV1Mapping[tokenId] != 0) { + return mintPriceWithPotion; + } else { + return mintPrice; + } + } + + function canBeRefunded(uint256 tokenId) public view returns (bool) { + return + !hasRefunded[tokenId] && + !isOwnerMint[tokenId] && + !hasRevokedRefund[tokenId] && + isRefundGuaranteeActive(); + } + function getRefundGuaranteeEndTime() public view returns (uint256) { return refundEndTime; } + function isRefundGuaranteeActive() public view returns (bool) { return (block.timestamp <= refundEndTime); } + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC2981, ERC721A) + returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + interfaceId == type(IERC2981).interfaceId || + super.supportsInterface(interfaceId); + } + function withdraw() external onlyOwner { require(block.timestamp > refundEndTime, "Refund period not over"); uint256 balance = address(this).balance; Address.sendValue(payable(owner()), balance); } + function setDefaultRoyalty(address receiver, uint96 feeNumerator) + external + onlyOwner + { + _setDefaultRoyalty(receiver, feeNumerator); + } + + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 feeNumerator + ) external onlyOwner { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } + function _baseURI() internal view override returns (string memory) { return baseURI; } diff --git a/contracts/cryptofighters/IERC721R.sol b/contracts/cryptofighters/IERC721R.sol new file mode 100644 index 0000000..5020a25 --- /dev/null +++ b/contracts/cryptofighters/IERC721R.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// Creator: Exo Digital Labs + +pragma solidity ^0.8.4; + +interface IERC721R { + event Refund( + address indexed _sender, + uint256 indexed _tokenId, + uint256 _amount + ); + + function refund(uint256[] calldata tokenIds) external; + + function getRefundPrice(uint256 tokenId) external view returns (uint256); + + function getRefundGuaranteeEndTime() external view returns (uint256); + + function isRefundGuaranteeActive() external view returns (bool); +}