// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; /// @notice Fixed-supply, sequential mint. One NFT per mint call, no token approvals. contract NovarysMint is ERC721, Ownable2Step, ReentrancyGuard { uint256 public immutable maxSupply; uint256 public immutable mintPrice; address payable public immutable treasury; uint256 public totalSupply; bool public saleActive; string public baseTokenURI; event SaleStateChanged(bool active); event Withdrawn(address indexed recipient, uint256 amount); error SaleClosed(); error SoldOut(); error IncorrectPayment(); error InvalidConfiguration(); error WithdrawalFailed(); constructor(string memory name_, string memory symbol_, uint256 supply_, uint256 price_, string memory uri_, address owner_, address payable treasury_) ERC721(name_, symbol_) Ownable(owner_) { bytes memory u = bytes(uri_); if (supply_ == 0 || supply_ > 500 || treasury_ == address(0) || u.length < 9 || u[u.length-1] != bytes1('/')) revert InvalidConfiguration(); if (u[0] != 'i' || u[1] != 'p' || u[2] != 'f' || u[3] != 's' || u[4] != ':' || u[5] != '/' || u[6] != '/') revert InvalidConfiguration(); maxSupply = supply_; mintPrice = price_; baseTokenURI = uri_; treasury = treasury_; } function mint() external payable nonReentrant { if (!saleActive) revert SaleClosed(); if (totalSupply >= maxSupply) revert SoldOut(); if (msg.value != mintPrice) revert IncorrectPayment(); uint256 id = ++totalSupply; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { _requireOwned(id); return string.concat(baseTokenURI, Strings.toString(id), '.json'); } function renounceOwnership() public override onlyOwner { revert InvalidConfiguration(); } function setSaleActive(bool active) external onlyOwner { saleActive = active; emit SaleStateChanged(active); } function withdraw() external nonReentrant onlyOwner { uint256 amount = address(this).balance; (bool ok,) = treasury.call{value:amount}(""); if (!ok) revert WithdrawalFailed(); emit Withdrawn(treasury, amount); } }