Files
simplex-chat/eth/nft/contracts/NFTNumbered.sol
T
Evgeny 6138c8e66b tokens: ERC1155 contract for tokens (#6376)
* tokens: ERC1155 contract for tokens

* simplify 1155

* NFT and minter contracts

* update NFT

* update

* update

* update

* update

* NFT metadata JSON

* update token metadata

* flattened contracts
2025-10-28 22:12:47 +00:00

122 lines
4.4 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "@openzeppelin/contracts@5.4.0/token/ERC721/extensions/ERC721Enumerable.sol";
import {IERC721} from "@openzeppelin/contracts@5.4.0/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts@5.4.0/access/Ownable.sol";
/// @title NFTNumbered
/// @notice ERC721 contract with sequential variants for immutable metadata for minted tokens, allowing to change metadata for future tokens.
/// @dev Non-upgradeable. Minter address is settable by owner. Global sequential token ID.
contract NFTNumbered is ERC721Enumerable, Ownable {
address public minter;
bool public mintingLocked;
TokenInfo[] private tokenInfos;
uint public nextTokenId = 1;
struct TokenInfo {
uint tokenId;
string tokenUri;
}
constructor(
string memory _name,
string memory _symbol,
string memory _tokenUri
) ERC721(_name, _symbol) Ownable(msg.sender) {
minter = msg.sender;
mintingLocked = false;
setNextTokenURI(_tokenUri);
}
event MinterUpdated(address indexed newMinter);
event MintingLocked();
event NextTokenURI(uint nextTokenId, string nextTokenUri);
/// @notice Updates the minter address.
function setMinter(address newMinter) external onlyOwner whenNotLocked {
minter = newMinter;
emit MinterUpdated(newMinter);
}
/// @notice Permanently disable minting and changes.
function lockMintingPermanently() external onlyOwner {
mintingLocked = true;
emit MintingLocked();
}
modifier whenNotLocked() {
require(!mintingLocked, "Contract permanently locked for changes and minting");
_;
}
/// @notice Sets tokenUri for future mints only.
function setNextTokenURI(string memory newTokenUri) public onlyOwner whenNotLocked {
uint len = tokenInfos.length;
TokenInfo memory newInfo = TokenInfo({tokenId: nextTokenId, tokenUri: newTokenUri});
if (len == 0 || tokenInfos[len - 1].tokenId < nextTokenId) { // add
tokenInfos.push(newInfo);
} else { // replace
tokenInfos[len - 1] = newInfo;
}
emit NextTokenURI(nextTokenId, newTokenUri);
}
/// @notice metadata URI of the token that will be minted next
function nextTokenURI() external view returns (string memory) {
return tokenInfos[tokenInfos.length - 1].tokenUri;
}
/// @notice Mints a new token.
/// @param to The recipient of the token.
function mint(address to) external whenNotLocked {
require(msg.sender == owner() || msg.sender == minter, "Caller must be the owner or minter");
require(to != address(0), "Cannot mint to zero address");
uint tokenId = nextTokenId++;
_safeMint(to, tokenId);
}
/// @notice Burn owned token.
function burn(uint tokenId) external {
_burn(tokenId);
}
/// @notice prohibit approvals
// address to, uint256 tokenId
function approve(address, uint256) public virtual override (ERC721, IERC721) {
revert("Soulbound token: approvals prohibited");
}
/// @notice prohibit approvals
// address operator, bool approved
function setApprovalForAll(address, bool) public virtual override (ERC721, IERC721) {
revert("Soulbound token: approvals prohibited");
}
/// @notice Limits ownership to 1 token.
function _update(address to, uint tokenId, address auth) internal virtual override returns (address) {
require(to == address(0) || balanceOf(to) == 0, "Soulbound token: only 1 per address");
require(to == address(0) || _ownerOf(tokenId) == address(0), "Soulbound token: transfers prohibited");
return super._update(to, tokenId, auth);
}
/// @notice Returns embedded JSON metadata URI.
/// @param id Token ID.
function tokenURI(uint id) public view virtual override returns (string memory) {
_requireOwned(id);
uint len = tokenInfos.length;
for (uint i = len; i > 0; ) {
unchecked { i--; }
if (tokenInfos[i].tokenId > id) continue;
return tokenInfos[i].tokenUri;
}
revert("Unknown token ID");
}
/// @notice Withdraw any accidental ETH.
function withdraw() external onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Withdraw failed");
}
}