mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-21 19:15:56 +00:00
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:
2
eth/nft/.gitignore
vendored
Normal file
2
eth/nft/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.deps
|
||||
artifacts/
|
||||
38
eth/nft/.prettierrc.json
Normal file
38
eth/nft/.prettierrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
eth/nft/compiler_config.json
Normal file
16
eth/nft/compiler_config.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
eth/nft/contracts/MultiERC1155.sol
Normal file
200
eth/nft/contracts/MultiERC1155.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
92
eth/nft/contracts/NFTMinter.sol
Normal file
92
eth/nft/contracts/NFTMinter.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
4100
eth/nft/contracts/NFTMinter_flattened.sol
Normal file
4100
eth/nft/contracts/NFTMinter_flattened.sol
Normal file
File diff suppressed because it is too large
Load Diff
121
eth/nft/contracts/NFTNumbered.sol
Normal file
121
eth/nft/contracts/NFTNumbered.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
3892
eth/nft/contracts/NFTNumbered_flattened.sol
Normal file
3892
eth/nft/contracts/NFTNumbered_flattened.sol
Normal file
File diff suppressed because it is too large
Load Diff
10
eth/nft/scripts/deploy_with_ethers.ts
Normal file
10
eth/nft/scripts/deploy_with_ethers.ts
Normal 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)
|
||||
}
|
||||
})()
|
||||
336
eth/nft/tests/MultiERC1155_test.sol
Normal file
336
eth/nft/tests/MultiERC1155_test.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
47
eth/nft/tests/NFTMinter_test.sol
Normal file
47
eth/nft/tests/NFTMinter_test.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
82
eth/nft/tests/NFTNumbered_test.sol
Normal file
82
eth/nft/tests/NFTNumbered_test.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user