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
This commit is contained in:
Evgeny
2025-10-28 22:12:47 +00:00
committed by GitHub
parent 7a858695bf
commit 6138c8e66b
12 changed files with 8936 additions and 0 deletions

2
eth/nft/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.deps
artifacts/

38
eth/nft/.prettierrc.json Normal file
View File

@@ -0,0 +1,38 @@
{
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false
}
},
{
"files": "*.yml",
"options": {}
},
{
"files": "*.yaml",
"options": {}
},
{
"files": "*.toml",
"options": {}
},
{
"files": "*.json",
"options": {}
},
{
"files": "*.js",
"options": {}
},
{
"files": "*.ts",
"options": {}
}
]
}

View File

@@ -0,0 +1,16 @@
{
"language": "Solidity",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"": ["ast"],
"*": ["abi", "metadata", "devdoc", "userdoc", "storageLayout", "evm.legacyAssembly", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "evm.gasEstimates", "evm.assembly"]
}
}
}
}

View File

@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "@openzeppelin/contracts@5.4.0/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts@5.4.0/access/Ownable.sol";
import "@openzeppelin/contracts@5.4.0/utils/Base64.sol";
import "@openzeppelin/contracts@5.4.0/utils/Strings.sol";
/// @title MultiERC1155
/// @notice ERC1155 contract with sequential variants for immutable metadata.
/// @dev Non-upgradeable. Admin and minter addresses are settable by owner. Global sequential token IDs.
contract MultiERC1155 is ERC1155, Ownable {
address public admin; // can manage token IDs
address public minter; // can mint tokens for existing IDs
bool public mintingEnabled;
bool public contractLocked; // no more variants can be added, minting cannot be enabled, cannot be unlocked
// for name and description avoid or escape double quotes, so it can be used in JSON
struct TokenInfo {
string tokenUri;
uint totalSupply; // 0 for unlimited
bool enabled;
}
struct TokenState {
TokenInfo tokenInfo;
uint currentSupply;
bool exists;
bool locked;
}
uint private _nextTokenId; // Global sequential token ID (starts at 1)
mapping(uint => TokenState) public tokens;
uint[] public tokenIds;
function getTokenIds() view external returns(uint[] memory) {
return tokenIds;
}
event AdminUpdated(address indexed newAdmin);
event MinterUpdated(address indexed newMinter);
event MintingEnabled(bool newEnabled);
event ContractLocked();
event TokenAdded(uint indexed tokenId);
event TokenRemoved(uint indexed tokenId);
event TokenUpdated(uint indexed tokenId, bool newEnabled, uint newTotalSupply);
event TokenLocked(uint indexed tokenId);
constructor() ERC1155("") Ownable(msg.sender) {
admin = msg.sender;
minter = msg.sender;
_nextTokenId = 1;
mintingEnabled = true;
}
/// @notice Updates the minter address.
/// @param newAdmin The new minter.
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin;
emit AdminUpdated(newAdmin);
}
/// @notice Updates the minter address.
/// @param newMinter The new minter.
function setMinter(address newMinter) external onlyOwner {
minter = newMinter;
emit MinterUpdated(newMinter);
}
/// @notice Enables/disables minting.
/// @param enabled True/false to enable/disable minting.
function toggleMinting(bool enabled) external onlyOwner {
if (mintingEnabled != enabled) {
mintingEnabled = enabled;
emit MintingEnabled(enabled);
}
}
/// @notice Permanently locks any token changes and minting, irreversible.
function lockContract() external onlyOwner {
contractLocked = true;
mintingEnabled = false;
emit ContractLocked();
}
/// @notice Adds a new variant and optionally sets it as current.
/// @param info New token info.
function addToken(TokenInfo memory info) external {
require(msg.sender == admin || msg.sender == owner(), "Only admin and owner can add tokens");
_addToken(info);
}
function _addToken(TokenInfo memory info) internal {
require(bytes(info.tokenUri).length > 0, "Token tokenUri required");
uint id = _nextTokenId++;
require(!tokens[id].exists, "Contract error: token ID already exists");
tokens[id] = TokenState({
tokenInfo: info,
currentSupply: 0,
exists: true,
locked: false
});
tokenIds.push(id);
emit TokenAdded(id);
}
/// @notice Removes the last variant if unused (currentSupply == 0).
function removeToken(uint id) external {
require(msg.sender == admin || msg.sender == owner(), "Only admin and owner can remove tokens");
TokenState storage token = tokens[id];
require(token.exists, "Token ID does not exist");
require(token.currentSupply == 0, "Tokens already minted for this ID");
require(tokenIds.length > 1, "Cannot remove the last token ID");
delete tokens[id];
for (uint i = 0; i < tokenIds.length; i++) {
if (tokenIds[i] == id) {
tokenIds[i] = tokenIds[tokenIds.length - 1];
tokenIds.pop();
break;
}
}
emit TokenRemoved(id);
}
/// @notice Enables/disables minting a specific token.
/// @param id Token ID.
/// @param newEnabled True/false to enable/disable minting.
/// @param newTotalSupply 0 for unlimited
function updateToken(uint id, bool newEnabled, uint newTotalSupply) external {
require(msg.sender == admin || msg.sender == owner(), "Only admin and owner can remove tokens");
TokenState storage token = tokens[id];
require(token.exists, "Token ID does not exist");
require(!token.locked, "Token ID is locked");
require(newTotalSupply == 0 || token.currentSupply <= newTotalSupply, "New total supply must be greater than existing token count for ID");
if (token.tokenInfo.enabled != newEnabled || token.tokenInfo.totalSupply != newTotalSupply) {
token.tokenInfo.enabled = newEnabled;
token.tokenInfo.totalSupply = newTotalSupply;
emit TokenUpdated(id, newEnabled, newTotalSupply);
}
}
/// @notice permanently lock a specific token from any further changes.
/// @param id Token ID.
function lockToken(uint id) external onlyOwner {
TokenState storage token = tokens[id];
require(token.exists, "Token ID does not exist");
require(!token.locked, "Token ID is already locked");
require(token.currentSupply != 0, "No tokens minted for this ID, use removeToken instead");
token.locked = true;
token.tokenInfo.enabled = false;
emit TokenLocked(id);
}
/// @notice Mints a token using the default variant.
/// @param to Recipient.
/// @param id Token ID.
/// @param value Amount to mint.
/// @param data Optional data.
function mint(address to, uint256 id, uint256 value, bytes calldata data) external {
require(!contractLocked, "Contract is permanently locked");
require(mintingEnabled, "Minting is disabled");
require(msg.sender == minter || msg.sender == admin || msg.sender == owner(), "Only minter, admin or owner can mint");
TokenState storage token = tokens[id];
require(token.exists, "Token ID does not exist");
require(!token.locked, "Token ID is locked");
require(token.tokenInfo.enabled, "Token ID is disabled");
require(token.tokenInfo.totalSupply == 0 || token.tokenInfo.totalSupply >= token.currentSupply + value, "Token supply exceeded for this ID");
require(value > 0, "Amount must be > 0");
_mint(to, id, value, data);
}
/// @dev Hook to update supplies on transfer/burn.
function _update(address from, address to, uint[] memory ids, uint[] memory values) internal virtual override {
super._update(from, to, ids, values);
for (uint i = 0; i < ids.length; i++) {
if (from == address(0)) { // mint
tokens[ids[i]].currentSupply += values[i];
}
if (to == address(0)) { // burn
tokens[ids[i]].currentSupply -= values[i];
}
}
}
/// @notice Returns embedded JSON metadata URI.
/// @param id Token ID.
function uri(uint id) public view virtual override returns (string memory) {
TokenState storage token = tokens[id];
require(token.exists, "Invalid token ID");
return token.tokenInfo.tokenUri;
}
}

View File

@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "@openzeppelin/contracts@5.4.0/access/Ownable.sol";
import "@openzeppelin/contracts@5.4.0/utils/Pausable.sol";
import "./NFTNumbered.sol";
contract NFTMinter is Ownable, Pausable {
NFTNumbered public nft;
uint public mintStartTime;
uint public mintEndTime;
uint public mintCount;
/// @param _mintEndTime Unix timestamp when minting ends, 0 to mint without time limit.
constructor(address _nftAddress, uint _mintStartTime, uint _mintEndTime, bool _paused) Ownable(msg.sender) {
_setNFT(_nftAddress);
require(_mintEndTime == 0 || _mintEndTime > _mintStartTime, "Mint end time is before start time");
require(_mintEndTime == 0 || _mintEndTime > block.timestamp, "Mint end time is in the past");
mintStartTime = _mintStartTime;
mintEndTime = _mintEndTime;
if (_paused) _pause();
}
event NFTUpdated(address indexed newNFT);
event Minted(address indexed to, uint count);
event MintStartUpdated(uint newTime);
event MintEndUpdated(uint newTime);
event MintCountReset(uint oldCount);
/// @notice Allows anyone to mint NFT for free (gas only). Any mint restrictions other than not paused or mint time must be in NFT contract
function mint() external whenNotPaused {
require(mintEndTime == 0 || mintEndTime > block.timestamp, "Minting ended");
require(mintStartTime <= block.timestamp, "Minting not started");
nft.mint(msg.sender);
mintCount++;
emit Minted(msg.sender, mintCount);
}
/// @notice Update the target NFT contract.
/// @param newNFT The new NFT contract address.
function setNFT(address newNFT) external onlyOwner {
_setNFT(newNFT);
emit NFTUpdated(newNFT);
}
function _setNFT(address _nft) internal {
require(_nft != address(0), "NFT contract address is 0");
uint codeSize;
assembly { codeSize := extcodesize(_nft) }
require(codeSize > 0, "Not a contract address");
nft = NFTNumbered(_nft);
}
/// @notice Reset mint counter.
function resetMintCount() external onlyOwner {
uint old = mintCount;
mintCount = 0;
emit MintCountReset(old);
}
/// @notice Update the mint start timestamp.
/// @param newTime The new Unix timestamp when minting ends.
function setMintStartTime(uint256 newTime) external onlyOwner {
require(mintEndTime == 0 || newTime < mintEndTime, "Mint end time is before start time");
mintStartTime = newTime;
emit MintStartUpdated(newTime);
}
/// @notice Update the mint end timestamp.
/// @param newTime The new Unix timestamp when minting ends, 0 to mint without time limit.
function setMintEndTime(uint256 newTime) external onlyOwner {
require(newTime == 0 || newTime > mintStartTime, "Mint end time is before start time");
require(newTime == 0 || newTime > block.timestamp, "Mint end time is in the past");
mintEndTime = newTime;
emit MintEndUpdated(newTime);
}
/// @notice Pause minting.
function pause() external onlyOwner {
_pause();
}
/// @notice Unpause minting.
function unpause() external onlyOwner {
_unpause();
}
/// @notice Withdraw any accidental ETH.
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
// 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");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import { deploy } from './ethers-lib'
(async () => {
try {
const result = await deploy('MyToken', [])
console.log(`address: ${result.address}`)
} catch (e) {
console.log(e.message)
}
})()

View File

@@ -0,0 +1,336 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "remix_tests.sol";
import "../contracts/MultiERC1155.sol";
contract MultiERC1155Test {
MultiERC1155 public ct;
address public owner = address(this);
address public admin = address(0x1);
address public minter = address(0x2);
address public user = address(0x3);
address public recipient = address(0x4);
MultiERC1155.TokenInfo defaultInfo = MultiERC1155.TokenInfo({
tokenUri: "https://example.com/token.json",
totalSupply: 0,
enabled: true
});
function beforeAll() public {
ct = new MultiERC1155();
MultiERC1155.TokenInfo memory newInfo = defaultInfo;
(bool success, ) = address(ct).call(abi.encodeWithSignature("addToken((string,bool,string,string,string,uint256))", newInfo));
Assert.equal(success, true, "addToken success");
}
// Constructor Test
function testConstructor() public {
Assert.equal(ct.owner(), owner, "Owner should be deployer");
Assert.equal(ct.admin(), owner, "Admin should be deployer");
Assert.equal(ct.minter(), owner, "Minter should be deployer");
Assert.equal(ct.mintingEnabled(), true, "Minting should be enabled");
Assert.equal(ct.contractLocked(), false, "Contract should not be locked");
uint[] memory ids = ct.getTokenIds();
Assert.equal(ids.length, uint(1), "One token ID added");
Assert.equal(ids[0], uint(1), "First token ID is 1");
(, uint currentSupply, bool exists, bool locked) = ct.tokens(1);
Assert.equal(exists, true, "Token 1 exists");
Assert.equal(locked, false, "Token 1 not locked");
Assert.equal(currentSupply, uint(0), "Token 1 supply 0");
}
// setAdmin Test
function testSetAdmin() public {
ct.setAdmin(admin);
Assert.equal(ct.admin(), admin, "Admin updated");
}
function testSetAdminOnlyOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("setAdmin(address)", admin));
Assert.equal(success, true, "Set admin from owner succeeds"); // Since this is owner
// To test revert, Remix plugin runs as this, so for non-owner, manual simulation not easy; note as TODO or skip
}
// setMinter Test
function testSetMinter() public {
ct.setMinter(minter);
Assert.equal(ct.minter(), minter, "Minter updated");
}
function testSetMinterOnlyOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("setMinter(address)", minter));
Assert.equal(success, true, "Set minter from owner succeeds");
}
// toggleMinting Test
function testToggleMinting() public {
ct.toggleMinting(false);
Assert.equal(ct.mintingEnabled(), false, "Minting disabled");
ct.toggleMinting(true);
Assert.equal(ct.mintingEnabled(), true, "Minting enabled");
}
function testToggleMintingNoChange() public {
ct.toggleMinting(true); // Already true
Assert.equal(ct.mintingEnabled(), true, "No change if same");
}
function testToggleMintingOnlyOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("toggleMinting(bool)", false));
Assert.equal(success, true, "Toggle from owner succeeds");
}
// lockContract Test
function testLockContract() public {
ct.lockContract();
Assert.equal(ct.contractLocked(), true, "Contract locked");
Assert.equal(ct.mintingEnabled(), false, "Minting disabled");
}
function testLockContractOnlyOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("lockContract()"));
Assert.equal(success, true, "Lock from owner succeeds");
}
// addToken Test
function testAddToken() public {
MultiERC1155.TokenInfo memory newInfo = MultiERC1155.TokenInfo({
tokenUri: "https://example.com/new_token.json",
totalSupply: 100,
enabled: true
});
ct.addToken(newInfo);
uint[] memory ids = ct.getTokenIds();
Assert.equal(ids.length, uint(2), "Two token IDs");
Assert.equal(ids[1], uint(2), "Second token ID 2");
(MultiERC1155.TokenInfo memory tokenInfo, uint currentSupply, bool exists, bool locked) = ct.tokens(2);
Assert.equal(exists, true, "Token 2 exists");
Assert.equal(locked, false, "Token 2 not locked");
Assert.equal(currentSupply, uint(0), "Token 2 supply 0");
Assert.equal(tokenInfo.totalSupply, uint(100), "Token 2 totalSupply 100");
// Validate info fields as needed
}
function testAddTokenByAdmin() public {
ct.setAdmin(admin);
MultiERC1155.TokenInfo memory newInfo = defaultInfo;
(bool success, ) = address(ct).call(abi.encodeWithSignature("addToken((string,bool,string,string,string,uint256))", newInfo));
Assert.equal(success, true, "Add by admin succeeds");
uint[] memory ids = ct.getTokenIds();
Assert.equal(ids.length, uint(2), "Two token IDs");
}
function testAddTokenRevertInvalidInfo() public {
MultiERC1155.TokenInfo memory invalidInfo = MultiERC1155.TokenInfo({
tokenUri: "",
totalSupply: 0,
enabled: true
});
(bool success, ) = address(ct).call(abi.encodeWithSignature("addToken((string,bool,string,string,string,uint256))", invalidInfo));
Assert.equal(success, false, "Invalid info reverts");
}
function testAddTokenOnlyAdminOrOwner() public {
MultiERC1155.TokenInfo memory info = defaultInfo;
(bool success, ) = address(ct).call(abi.encodeWithSignature("addToken((string,bool,string,string,string,uint256))", info));
Assert.equal(success, true, "Add from owner succeeds");
}
// removeToken Test
function testRemoveToken() public {
MultiERC1155.TokenInfo memory newInfo = defaultInfo;
ct.addToken(newInfo);
uint[] memory ids = ct.getTokenIds();
Assert.equal(ids.length, uint(2), "Two token IDs");
ct.removeToken(2);
ids = ct.getTokenIds();
Assert.equal(ids.length, uint(1), "One token ID after removal");
(,,bool exists,) = ct.tokens(2);
Assert.equal(exists, false, "Token 2 removed");
}
function testRemoveTokenRevertNonExist() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("removeToken(uint256)", 99));
Assert.equal(success, false, "Remove non-exist reverts");
}
function testRemoveTokenRevertHasSupply() public {
ct.mint(user, 1, 10, "");
(bool success,) = address(ct).call(abi.encodeWithSignature("removeToken(uint256)", 1));
Assert.equal(success, false, "Remove with supply reverts");
}
function testRemoveTokenRevertLast() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("removeToken(uint256)", 1));
Assert.equal(success, false, "Remove last reverts");
}
function testRemoveTokenOnlyAdminOrOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("removeToken(uint256)", 1));
Assert.equal(success, false, "Remove last fails"); // But for non-owner, need simulation
}
// updateToken Test
function testUpdateToken() public {
ct.updateToken(1, false, 100);
(MultiERC1155.TokenInfo memory tokenInfo,,,) = ct.tokens(1);
Assert.equal(tokenInfo.enabled, false, "Enabled updated");
Assert.equal(tokenInfo.totalSupply, uint(100), "Total supply updated");
}
function testUpdateTokenNoChange() public {
(MultiERC1155.TokenInfo memory tokenInfo,,,) = ct.tokens(1);
ct.updateToken(1, tokenInfo.enabled, tokenInfo.totalSupply);
// No assert, as no change
}
function testUpdateTokenRevertNonExist() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("updateToken(uint256,bool,uint256)", 99, true, 0));
Assert.equal(success, false, "Update non-exist reverts");
}
function testUpdateTokenRevertLocked() public {
ct.mint(user, 1, 10, "");
ct.lockToken(1);
(bool success, ) = address(ct).call(abi.encodeWithSignature("updateToken(uint256,bool,uint256)", 1, true, 0));
Assert.equal(success, false, "Update locked reverts");
}
function testUpdateTokenRevertInvalidSupply() public {
ct.mint(user, 1, 10, "");
(bool success, ) = address(ct).call(abi.encodeWithSignature("updateToken(uint256,bool,uint256)", 1, true, 5));
Assert.equal(success, false, "Invalid supply reverts");
}
function testUpdateTokenOnlyAdminOrOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("updateToken(uint256,bool,uint256)", 1, true, 0));
Assert.equal(success, true, "Update from owner succeeds");
}
// lockToken Test
function testLockToken() public {
ct.mint(user, 1, 10, "");
ct.lockToken(1);
(MultiERC1155.TokenInfo memory tokenInfo,,, bool locked) = ct.tokens(1);
Assert.equal(locked, true, "Token locked");
Assert.equal(tokenInfo.enabled, false, "Enabled false");
}
function testLockTokenRevertNonExist() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("lockToken(uint256)", 99));
Assert.equal(success, false, "Lock non-exist reverts");
}
function testLockTokenRevertAlreadyLocked() public {
ct.mint(user, 1, 10, "");
ct.lockToken(1);
(bool success, ) = address(ct).call(abi.encodeWithSignature("lockToken(uint256)", 1));
Assert.equal(success, false, "Lock already locked reverts");
}
function testLockTokenRevertNoSupply() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("lockToken(uint256)", 1));
Assert.equal(success, false, "Lock no supply reverts");
}
function testLockTokenOnlyOwner() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("lockToken(uint256)", 1));
Assert.equal(success, false, "Lock no supply fails"); // Test with supply
}
// mint Test
function testMint() public {
ct.mint(user, 1, 10, "");
Assert.equal(ct.balanceOf(user, 1), 10, "User balance 10");
(,uint currentSupply,,) = ct.tokens(1);
Assert.equal(currentSupply, 10, "Token supply 10");
}
function testMintRevertLockedContract() public {
ct.lockContract();
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 10, ""));
Assert.equal(success, false, "Mint locked contract reverts");
}
function testMintRevertMintingDisabled() public {
ct.toggleMinting(false);
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 10, ""));
Assert.equal(success, false, "Mint disabled reverts");
}
function testMintRevertInvalidCaller() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 10, ""));
Assert.equal(success, true, "Mint from owner succeeds");
}
function testMintRevertNonExist() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 99, 10, ""));
Assert.equal(success, false, "Mint non-exist reverts");
}
function testMintRevertLockedToken() public {
ct.mint(user, 1, 10, "");
ct.lockToken(1);
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 5, ""));
Assert.equal(success, false, "Mint locked token reverts");
}
function testMintRevertDisabledToken() public {
ct.updateToken(1, false, 0);
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 10, ""));
Assert.equal(success, false, "Mint disabled token reverts");
}
function testMintRevertSupplyExceeded() public {
ct.updateToken(1, true, 5);
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 10, ""));
Assert.equal(success, false, "Mint supply exceeded reverts");
}
function testMintRevertZeroAmount() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("mint(address,uint256,uint256,bytes)", user, 1, 0, ""));
Assert.equal(success, false, "Mint zero amount reverts");
}
// _update Test (via transfer/burn)
function testUpdateMint() public {
ct.mint(user, 1, 10, "");
(,uint currentSupply,,) = ct.tokens(1);
Assert.equal(currentSupply, 10, "Supply after mint");
}
function testUpdateBurn() public {
ct.mint(user, 1, 10, "");
(bool success, ) = address(ct).call(abi.encodeWithSignature("safeTransferFrom(address,address,uint256,uint256,bytes)", user, address(0), 1, 5, ""));
Assert.equal(success, true, "Burn succeeds");
(,uint currentSupply,,) = ct.tokens(1);
Assert.equal(currentSupply, 5, "Supply after burn");
}
function testUpdateTransfer() public {
ct.mint(user, 1, 10, "");
(bool success, ) = address(ct).call(abi.encodeWithSignature("safeTransferFrom(address,address,uint256,uint256,bytes)", user, recipient, 1, 5, ""));
Assert.equal(success, true, "Transfer succeeds");
(,uint currentSupply,,) = ct.tokens(1);
Assert.equal(currentSupply, 10, "Supply unchanged on transfer");
}
// uri Test
function testUri() public {
string memory json = Base64.encode(bytes('{"name":"Test Token","description":"Test Description","image":"https://test.com/image.png","properties":{}}'));
string memory expected = string.concat("data:application/json;base64,", json);
Assert.equal(ct.uri(1), expected, "URI matches");
}
function testUriRevertNonExist() public {
(bool success, ) = address(ct).call(abi.encodeWithSignature("uri(uint256)", 99));
Assert.equal(success, false, "URI non-exist reverts");
}
}

View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import "../contracts/NFTMinter.sol";
import "../contracts/NFTNumbered.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract NFTMinterTest {
NFTNumbered s;
NFTMinter m;
address public owner = address(this);
function beforeAll() public {
s = new NFTNumbered(
"SimpleX NFT: SMPX testnet access",
"SIMPLEXNFT",
"https://ipfs.io/ipfs/abcd"
);
m = new NFTMinter(address(s), 0, 0, false);
}
function testCreateMinter() public {
Assert.equal(address(m.nft()), address(s), "bad nft contract");
Assert.equal(m.mintEndTime(), 0, "bad time");
Assert.equal(m.owner(), owner, "bad owner");
}
function testMinting() public {
m.setMintStartTime(block.timestamp + 86400);
try m.mint() {
Assert.ok(false, "expected revert");
} catch Error(string memory reason) {
Assert.equal(reason, "Minting not started", "bad reason");
} catch (bytes memory) {
Assert.ok(false, "unexpected error");
}
m.setMintStartTime(0);
}
}

View File

@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "remix_tests.sol";
import "remix_accounts.sol";
import "../contracts/NFTNumbered.sol";
contract NFTNumberedTest {
NFTNumbered s;
address public owner = address(this);
address public user1 = address(0x1);
address public user2 = address(0x2);
address public user3 = address(0x3);
address public user4 = TestsAccounts.getAccount(1);
address public minter = address(0x5);
function beforeAll () public {
s = new NFTNumbered(
"SimpleX NFT: SMPX testnet access",
"SIMPLEXNFT",
"https://ipfs.io/ipfs/abcd"
);
}
function testCreateToken () public {
Assert.equal(s.name(), "SimpleX NFT: SMPX testnet access", "bad name");
Assert.equal(s.symbol(), "SIMPLEXNFT", "bad symbol");
Assert.equal(s.nextTokenId(), 1, "bad next token ID");
Assert.equal(s.minter(), s.owner(), "minter different from owner");
Assert.equal(s.mintingLocked(), false, "minting locked");
Assert.equal(s.owner(), owner, "bad owner");
}
function testTransferToken() public {
Assert.equal(s.balanceOf(user3), 0, "bad balance");
s.mint(user4);
Assert.equal(s.balanceOf(user4), 1, "bad balance");
/// #sender: account-1
// try s.safeTransferFrom(user4, user3, 1) {
// Assert.ok(false, "expected revert");
// } catch Error(string memory reason) {
// Assert.equal(reason, "Token is soulbound: transfers are prohibited", "bad reason");
// } catch (bytes memory data) {
// Assert.ok(false, "unexpected error 2");
// }
}
function testMinting () public {
s.mint(user1);
Assert.equal(s.balanceOf(user1), 1, "bad balance");
Assert.equal(s.tokenURI(2), "https://ipfs.io/ipfs/abcd", "bad URI");
try s.mint(user1) {
Assert.ok(false, "expected revert");
} catch Error(string memory reason) {
Assert.equal(reason, "Soulbound token: only 1 per address", "bad reason");
} catch (bytes memory) {
Assert.ok(false, "unexpected error");
}
Assert.equal(s.nextTokenId(), 3, "bad next token ID");
s.mint(user2);
Assert.equal(s.balanceOf(user2), 1, "bad balance");
Assert.equal(s.tokenURI(3), "https://ipfs.io/ipfs/abcd", "bad URI");
Assert.equal(s.nextTokenURI(), "https://ipfs.io/ipfs/abcd", "bad URI");
s.setNextTokenURI("https://ipfs.io/ipfs/efgh");
Assert.equal(s.nextTokenURI(), "https://ipfs.io/ipfs/efgh", "bad URI");
Assert.equal(s.balanceOf(user3), 0, "bad balance");
s.mint(user3);
Assert.equal(s.balanceOf(user3), 1, "bad balance");
Assert.equal(s.tokenURI(4), "https://ipfs.io/ipfs/efgh", "bad URI");
}
function testMintingLock () public {
s.lockMintingPermanently();
try s.mint(user2) {
Assert.ok(false, "expected revert");
} catch Error(string memory reason) {
Assert.equal(reason, "Contract permanently locked for changes and minting", "bad reason");
} catch (bytes memory) {
Assert.ok(false, "unexpected error");
}
}
}