diff --git a/contracts/soulbounds/ERC1155RoyaltiesSoulbound.sol b/contracts/soulbounds/ERC1155RoyaltiesSoulbound.sol index 767e4be..78208e0 100644 --- a/contracts/soulbounds/ERC1155RoyaltiesSoulbound.sol +++ b/contracts/soulbounds/ERC1155RoyaltiesSoulbound.sol @@ -348,6 +348,29 @@ contract ERC1155RoyaltiesSoulbound is emit MintedId(to, id, amount, soulbound); } + function adminBatchMintById( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts, + bool soulbound + ) external onlyRole(MINTER_ROLE) whenNotPaused { + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 _id = tokenIds[i]; + uint256 _amount = amounts[i]; + isTokenExist(_id); + if (isTokenMintPaused[_id]) { + revert("TokenMintPaused"); + } + + if (soulbound) { + _soulbound(to, _id, _amount); + } + + _mint(to, _id, _amount, ""); + emit MintedId(to, _id, _amount, soulbound); + } + } + function _update( address from, address to, diff --git a/contracts/upgradeables/soulbounds/ERC1155RoyaltiesSoulboundV3.sol b/contracts/upgradeables/soulbounds/ERC1155RoyaltiesSoulboundV3.sol new file mode 100644 index 0000000..e83f27d --- /dev/null +++ b/contracts/upgradeables/soulbounds/ERC1155RoyaltiesSoulboundV3.sol @@ -0,0 +1,624 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +//TODO: This contract is deprecated USE THE ERC1155SoulboundV1.sol + +// @author Summon.xyz Team - https://summon.xyz +// @contributors: [ @ogarciarevett, @vasinl124] +//.................................................................................................................................................... +//....................&&&&&&.......................................................................................................................... +//..................&&&&&&&&&&&....................................................................................................................... +//..............X.....&&&&&&&&&&&&.................................................................................................................... +//............&&&&&&.....&&&&&&&&&&&.................................................................................................................. +//............&&&&&&&&&.....&&&&&..................................................................................................................... +//............&&&&&&&&&&&&.........&.............&&&&&&&&&&&&..&&&&....&&&&.&&&&&&&&..&&&&&&&.&&&&&&&&..&&&&&&&&.&&&&&&&&&&&&.&&&&&....&&&&........... +//...............&&&&&&&&&&&&.....&&$............&&&&..........&&&&....&&&&.&&&&&&&&.&&&&&&&&..&&&&&&&&.&&&&&&&&.&&&&&&&&&&&&.&&&&&&&..&&&&........... +//............&.....&&&&&&&&&&&&..................&&&&&&&&&&&..&&&&....&&&&.&&&&..&&&&&&.&&&&..&&&&.&&&&&&..&&&&.&&&&....&&&&.&&&&.&&&&&&&&........... +//............&&.......&&&&&&&&&&&&......................&&&&..&&&&&&&&&&&&.&&&&..&&&&&..&&&&..&&&&..&&&&...&&&&.&&&&&&&&&&&&.&&&&...&&&&&&........... +//................&&&.....&&&&&&&&&&+............&&&&&&&&&&&&...&&&&&&&&&&..&&&&...&&&&..&&&&.&&&&&..&&&&...&&&&.&&&&&&&&&&&&.&&&&....&&&&&........... +//.............&&&&&&&&&.....&&&&&&&.................................................................................................................. +//.............&&&&&&&&&&&&.....&&&................................................................................................................... +//.................&&&&&&&&&&&........................................................................................................................ +//....................&&&&&&&......................................................................................................................... +//.................................................................................................................................................... + +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { + ERC1155Upgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import { + ERC1155BurnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155BurnableUpgradeable.sol"; +import { + ERC1155SupplyUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; +import { + AccessControlUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + PausableUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { + ERC2981Upgradeable +} from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol"; +import { + ReentrancyGuardUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import { + Achievo1155SoulboundUpgradeable +} from "../ercs/extensions/Achievo1155SoulboundUpgradeable.sol"; +import { + ERCWhitelistSignatureUpgradeable +} from "../ercs/ERCWhitelistSignatureUpgradeable.sol"; +import { LibItems } from "../../libraries/LibItems.sol"; + +error InvalidSeed(); +error InvalidInput(); +error AddressIsZero(); +error ExceedMaxMint(); +error MissingRole(); +error TokenNotExist(); +error TokenMintPaused(); +error DuplicateID(); + +contract ERC1155RoyaltiesSoulboundV3 is + Initializable, + ERC1155BurnableUpgradeable, + ERC1155SupplyUpgradeable, + Achievo1155SoulboundUpgradeable, + ERC2981Upgradeable, + ERCWhitelistSignatureUpgradeable, + AccessControlUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable +{ + event ContractURIChanged(string indexed uri); + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant DEV_CONFIG_ROLE = keccak256("DEV_CONFIG_ROLE"); + + string public contractURI; + string private baseURI; + string public name; + string public symbol; + using Strings for uint256; + + uint256 public MAX_PER_MINT; + + mapping(uint256 => bool) private tokenExists; + mapping(uint256 => string) public tokenUris; // tokenId => tokenUri + mapping(uint256 => bool) public isTokenMintPaused; // tokenId => bool - default is false + + uint256[] public itemIds; + + mapping(address => mapping(uint256 => bool)) private tokenIdProcessed; + + modifier maxPerMintCheck(uint256 amount) { + if (amount > MAX_PER_MINT) { + revert ExceedMaxMint(); + } + _; + } + + event Minted( + address indexed to, + uint256[] tokenIds, + uint256 amount, + bool soulbound + ); + event MintedId( + address indexed to, + uint256 indexed tokenId, + uint256 amount, + bool soulbound + ); + event TokenAdded(uint256 indexed tokenId); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory _name, + string memory _symbol, + string memory _initBaseURI, + string memory _contractURI, + uint256 _maxPerMint, + bool _isPaused, + address devWallet + ) public initializer { + __ERC1155_init(""); + __ReentrancyGuard_init(); + __AccessControl_init(); + __Achievo1155SoulboundUpgradable_init(); + __ERCWhitelistSignatureUpgradeable_init(); + + if (devWallet == address(0)) { + revert AddressIsZero(); + } + + _grantRole(DEFAULT_ADMIN_ROLE, devWallet); + _grantRole(MINTER_ROLE, devWallet); + _grantRole(MANAGER_ROLE, devWallet); + _grantRole(DEV_CONFIG_ROLE, devWallet); + _addWhitelistSigner(devWallet); + + name = _name; + symbol = _symbol; + baseURI = _initBaseURI; + contractURI = _contractURI; + MAX_PER_MINT = _maxPerMint; + + if (_isPaused) _pause(); + } + + function getAllItems( + address _owner + ) public view returns (LibItems.TokenReturn[] memory) { + bool isAdmin = hasRole(MINTER_ROLE, _msgSender()); + if (!isAdmin && _owner != _msgSender()) { + revert MissingRole(); + } + uint256 totalTokens = itemIds.length; + LibItems.TokenReturn[] memory tokenReturns = new LibItems.TokenReturn[]( + totalTokens + ); + + uint index; + for (uint i = 0; i < totalTokens; i++) { + uint256 tokenId = itemIds[i]; + uint256 amount = balanceOf(_owner, tokenId); + + if (isAdmin || amount > 0) { + LibItems.TokenReturn memory tokenReturn = LibItems.TokenReturn({ + tokenId: tokenId, + tokenUri: uri(tokenId), + amount: amount + }); + tokenReturns[index] = tokenReturn; + index++; + } + } + + // truncate the array + LibItems.TokenReturn[] + memory returnsTruncated = new LibItems.TokenReturn[](index); + for (uint i = 0; i < index; i++) { + returnsTruncated[i] = tokenReturns[i]; + } + + return returnsTruncated; + } + + function isTokenExist(uint256 _tokenId) public view returns (bool) { + if (!tokenExists[_tokenId]) { + revert TokenNotExist(); + } + return true; + } + + function getChainID() public view returns (uint256) { + uint256 id; + assembly { + id := chainid() + } + return id; + } + + function _verifyContractChainIdAndDecode( + bytes calldata data + ) private view returns (uint256[] memory) { + uint256 currentChainId = getChainID(); + ( + address contractAddress, + uint256 chainId, + uint256[] memory tokenIds + ) = _decodeData(data); + + if (chainId != currentChainId || contractAddress != address(this)) { + revert InvalidSeed(); + } + return tokenIds; + } + + function decodeData( + bytes calldata _data + ) + public + view + onlyRole(DEV_CONFIG_ROLE) + returns (address, uint256, uint256[] memory) + { + return _decodeData(_data); + } + + function _decodeData( + bytes calldata _data + ) private view returns (address, uint256, uint256[] memory) { + ( + address contractAddress, + uint256 chainId, + uint256[] memory _itemIds + ) = abi.decode(_data, (address, uint256, uint256[])); + return (contractAddress, chainId, _itemIds); + } + + function pause() external onlyRole(MANAGER_ROLE) { + _pause(); + } + + function unpause() external onlyRole(MANAGER_ROLE) { + _unpause(); + } + + function addNewToken( + LibItems.TokenCreate calldata _token + ) public onlyRole(DEV_CONFIG_ROLE) { + if (bytes(_token.tokenUri).length > 0) { + tokenUris[_token.tokenId] = _token.tokenUri; + } + + if (_token.receiver != address(0)) { + _setTokenRoyalty( + _token.tokenId, + _token.receiver, + uint96(_token.feeBasisPoints) + ); + } + + tokenExists[_token.tokenId] = true; + + itemIds.push(_token.tokenId); + emit TokenAdded(_token.tokenId); + } + + function addNewTokens( + LibItems.TokenCreate[] calldata _tokens + ) external onlyRole(DEV_CONFIG_ROLE) { + for (uint256 i = 0; i < _tokens.length; i++) { + addNewToken(_tokens[i]); + } + } + + function updateTokenUri( + uint256 _tokenId, + string calldata _tokenUri + ) public onlyRole(DEV_CONFIG_ROLE) { + tokenUris[_tokenId] = _tokenUri; + } + + function batchUpdateTokenUri( + uint256[] calldata _tokenIds, + string[] calldata _tokenUris + ) public onlyRole(DEV_CONFIG_ROLE) { + if (_tokenIds.length != _tokenUris.length) { + revert InvalidInput(); + } + for (uint256 i = 0; i < _tokenIds.length; i++) { + updateTokenUri(_tokenIds[i], _tokenUris[i]); + } + } + + function updateTokenMintPaused( + uint256 _tokenId, + bool _isTokenMintPaused + ) public onlyRole(MANAGER_ROLE) { + isTokenMintPaused[_tokenId] = _isTokenMintPaused; + } + + function _mintBatch( + address to, + uint256[] memory _tokenIds, + uint256 amount, + bool soulbound + ) private { + for (uint256 i = 0; i < _tokenIds.length; i++) { + uint256 _id = _tokenIds[i]; + isTokenExist(_id); + if (isTokenMintPaused[_id]) { + revert TokenMintPaused(); + } + + if (soulbound) { + _soulbound(to, _id, amount); + } + + _mint(to, _id, amount, ""); + } + emit Minted(to, _tokenIds, amount, soulbound); + } + + function mint( + bytes calldata data, + uint256 amount, + bool soulbound, + uint256 nonce, + bytes calldata signature + ) + external + nonReentrant + signatureCheck(_msgSender(), nonce, data, signature) + maxPerMintCheck(amount) + whenNotPaused + { + uint256[] memory _tokenIds = _verifyContractChainIdAndDecode(data); + _mintBatch(_msgSender(), _tokenIds, amount, soulbound); + } + + function adminMint( + address to, + bytes calldata data, + bool soulbound + ) external onlyRole(MINTER_ROLE) whenNotPaused { + uint256[] memory _tokenIds = _verifyContractChainIdAndDecode(data); + _mintBatch(to, _tokenIds, 1, soulbound); + } + + function adminMintId( + address to, + uint256 id, + uint256 amount, + bool soulbound + ) external onlyRole(MINTER_ROLE) whenNotPaused { + isTokenExist(id); + + if (isTokenMintPaused[id]) { + revert TokenMintPaused(); + } + + if (soulbound) { + _soulbound(to, id, amount); + } + + _mint(to, id, amount, ""); + emit MintedId(to, id, amount, soulbound); + } + + function adminBatchMintById( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts, + bool soulbound + ) external onlyRole(MINTER_ROLE) whenNotPaused { + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 _id = tokenIds[i]; + uint256 _amount = amounts[i]; + isTokenExist(_id); + if (isTokenMintPaused[_id]) { + revert("TokenMintPaused"); + } + + if (soulbound) { + _soulbound(to, _id, _amount); + } + + _mint(to, _id, _amount, ""); + emit MintedId(to, _id, _amount, soulbound); + } + } + + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) { + super._update(from, to, ids, values); + } + + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) + public + virtual + override + soulboundCheckAndSync(_from, _to, _id, _amount, balanceOf(_from, _id)) + { + super.safeTransferFrom(_from, _to, _id, _amount, _data); + } + + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) + public + virtual + override + soulboundCheckAndSyncBatch( + _from, + _to, + _ids, + _amounts, + balanceOfBatchOneAccount(_from, _ids) + ) + { + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + + if (tokenIdProcessed[_from][id]) { + revert DuplicateID(); + } + + tokenIdProcessed[_from][id] = true; + } + + super.safeBatchTransferFrom(_from, _to, _ids, _amounts, _data); + + // Reset processed status after the transfer is completed + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + tokenIdProcessed[_from][id] = false; + } + } + + function balanceOfBatchOneAccount( + address account, + uint256[] memory ids + ) public view virtual returns (uint256[] memory) { + uint256[] memory batchBalances = new uint256[](ids.length); + + for (uint256 i = 0; i < ids.length; ++i) { + batchBalances[i] = balanceOf(account, ids[i]); + } + + return batchBalances; + } + + function burn( + address to, + uint256 tokenId, + uint256 amount + ) + public + virtual + override + nonReentrant + soulboundCheckAndSync( + to, + address(0), + tokenId, + amount, + balanceOf(to, tokenId) + ) + { + ERC1155BurnableUpgradeable.burn(to, tokenId, amount); + } + + function burnBatch( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts + ) + public + virtual + override + nonReentrant + soulboundCheckAndSyncBatch( + to, + address(0), + tokenIds, + amounts, + balanceOfBatchOneAccount(to, tokenIds) + ) + { + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 id = tokenIds[i]; + + if (tokenIdProcessed[to][id]) { + revert DuplicateID(); + } + + tokenIdProcessed[to][id] = true; + } + + ERC1155BurnableUpgradeable.burnBatch(to, tokenIds, amounts); + + // Reset processed status after the transfer is completed + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 id = tokenIds[i]; + tokenIdProcessed[to][id] = false; + } + } + + function supportsInterface( + bytes4 interfaceId + ) + public + view + override( + ERC1155Upgradeable, + ERC2981Upgradeable, + AccessControlUpgradeable + ) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function uri(uint256 tokenId) public view override returns (string memory) { + isTokenExist(tokenId); + if (bytes(tokenUris[tokenId]).length > 0) { + return tokenUris[tokenId]; + } else { + return string(abi.encodePacked(baseURI, "/", tokenId.toString())); + } + } + + function updateBaseUri( + string memory _baseURI + ) external onlyRole(DEV_CONFIG_ROLE) { + baseURI = _baseURI; + } + + function setRoyaltyInfo( + address receiver, + uint96 feeBasisPoints + ) external onlyRole(MANAGER_ROLE) { + _setDefaultRoyalty(receiver, feeBasisPoints); + } + + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 feeBasisPoints + ) external onlyRole(MANAGER_ROLE) { + _setTokenRoyalty(tokenId, receiver, uint96(feeBasisPoints)); + } + + function resetTokenRoyalty( + uint256 tokenId + ) external onlyRole(MANAGER_ROLE) { + _resetTokenRoyalty(tokenId); + } + + function updateWhitelistAddress( + address _address, + bool _isWhitelisted + ) external onlyRole(DEV_CONFIG_ROLE) { + _updateWhitelistAddress(_address, _isWhitelisted); + } + + function setContractURI( + string memory _contractURI + ) public onlyRole(DEV_CONFIG_ROLE) { + contractURI = _contractURI; + emit ContractURIChanged(_contractURI); + } + + function adminVerifySignature( + address to, + uint256 nonce, + bytes calldata data, + bytes calldata signature + ) public onlyRole(DEV_CONFIG_ROLE) returns (bool) { + return _verifySignature(to, nonce, data, signature); + } + + function addWhitelistSigner( + address _signer + ) external onlyRole(DEV_CONFIG_ROLE) { + _addWhitelistSigner(_signer); + } + + function removeWhitelistSigner( + address signer + ) external onlyRole(DEV_CONFIG_ROLE) { + _removeWhitelistSigner(signer); + } + + // Reserved storage space to allow for layout changes in the future. + uint256[37] private __gap; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index a6334a0..d853c64 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,7 +29,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 1000, + runs: 200, // need to reduce this so it can compile ERC1155RoyaltiesSoulboundV3 :'( details: { yul: true, }, diff --git a/test/ItemBoundV3.t.sol b/test/ItemBoundV3.t.sol new file mode 100644 index 0000000..3b6f49c --- /dev/null +++ b/test/ItemBoundV3.t.sol @@ -0,0 +1,1215 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import "forge-std/StdCheats.sol"; + +import { + ERC1967Proxy +} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { + IERC1155Errors +} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { + IAccessControl +} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import { LibItems, TestLibItems } from "../contracts/libraries/LibItems.sol"; +import { + ERC1155RoyaltiesSoulboundV3 +} from "../contracts/upgradeables/soulbounds/ERC1155RoyaltiesSoulboundV3.sol"; +import { + MockERC1155Receiver +} from "../contracts/mocks/MockERC1155Receiver.sol"; + +error InvalidSeed(); +error InvalidInput(); +error AddressIsZero(); +error ExceedMaxMint(); +error MissingRole(); +error TokenNotExist(); +error TokenMintPaused(); +error DuplicateID(); + +contract ItemBoundV3Test is StdCheats, Test { + using Strings for uint256; + + ERC1155RoyaltiesSoulboundV3 public itemBoundProxy; + + MockERC1155Receiver public mockERC1155Receiver; + + struct Wallet { + address addr; + uint256 privateKey; + } + + string public minterLabel = "minter"; + string public playerLabel = "player"; + string public player2Label = "player2"; + string public player3Label = "player3"; + + Wallet public minterWallet; + Wallet public playerWallet; + Wallet public playerWallet2; + Wallet public playerWallet3; + + uint256 public seed1 = 1234; + uint256 public seed2 = 4321; + uint256 public nonce; + bytes public signature; + bytes public encodedItems1; + uint256 public nonce2; + bytes public signature2; + bytes public encodedItems2; + + uint256 private _seed; + LibItems.TokenCreate[] public _tokens; + uint256[] public _tokenIds; + + uint256 public chainId = 31337; + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant DEV_CONFIG_ROLE = keccak256("DEV_CONFIG_ROLE"); + + function getWallet( + string memory walletLabel + ) public returns (Wallet memory) { + (address addr, uint256 privateKey) = makeAddrAndKey(walletLabel); + Wallet memory wallet = Wallet(addr, privateKey); + return wallet; + } + + function generateSignature( + address wallet, + bytes memory encodedItems, + string memory signerLabel + ) public returns (uint256, bytes memory) { + Wallet memory signerWallet = getWallet(signerLabel); + + uint256 _nonce = uint256( + keccak256( + abi.encodePacked( + block.timestamp, + block.prevrandao, + signerWallet.addr + ) + ) + ) % 50; + + bytes32 message = keccak256( + abi.encodePacked(wallet, encodedItems, _nonce) + ); + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(message); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + signerWallet.privateKey, + hash + ); + return (_nonce, abi.encodePacked(r, s, v)); + } + + function concatenateStrings( + string memory a, + string memory b + ) internal pure returns (string memory) { + return string(abi.encodePacked(a, b)); + } + + function generateRandomItemId() internal returns (uint256) { + _seed = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), _seed)) + ); + return _seed; + } + + function generateRandomLevel() internal returns (uint256) { + uint256 _seed = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), _seed)) + ); + return (_seed % 10) + 1; // 1 - 10 + } + + function generateRandomTier() internal returns (TestLibItems.Tier) { + uint256 _seed = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), _seed)) + ); + uint256 random = _seed % 5; // 0 - 4 + + if (random == 0) { + return TestLibItems.Tier.COMMON; + } else if (random == 1) { + return TestLibItems.Tier.UNCOMMON; + } else if (random == 2) { + return TestLibItems.Tier.RARE; + } else if (random == 3) { + return TestLibItems.Tier.LEGENDARY; + } else if (random == 4) { + return TestLibItems.Tier.MYTHICAL; + } else { + return TestLibItems.Tier.COMMON; + } + } + + function encode( + address contractAddress, + uint256[] memory itemIds + ) public view returns (bytes memory) { + return (abi.encode(contractAddress, chainId, itemIds)); + } + + function deployContract() public returns (ERC1155RoyaltiesSoulboundV3) { + ERC1155RoyaltiesSoulboundV3 itemBoundV3 = new ERC1155RoyaltiesSoulboundV3(); + ERC1967Proxy proxy = new ERC1967Proxy(address(itemBoundV3), ""); + ERC1155RoyaltiesSoulboundV3(address(proxy)).initialize( + "Test1155", + "T1155", + "MISSING_BASE_URL", + "MISSING_CONTRACT_URL", + 1, + false, + address(this) + ); + + return ERC1155RoyaltiesSoulboundV3(address(proxy)); + } + + function setUp() public { + playerWallet = getWallet(playerLabel); + playerWallet2 = getWallet(player2Label); + playerWallet3 = getWallet(player3Label); + minterWallet = getWallet(minterLabel); + + itemBoundProxy = deployContract(); + + itemBoundProxy.addWhitelistSigner(minterWallet.addr); + + mockERC1155Receiver = new MockERC1155Receiver(); + + for (uint256 i = 0; i < 1300; i++) { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: string( + abi.encodePacked( + "https://something.com", + "/", + _tokenId.toString() + ) + ), + receiver: address(0), + feeBasisPoints: 0 + }); + + _tokens.push(_token); + _tokenIds.push(_tokenId); + } + + itemBoundProxy.addNewTokens(_tokens); + + uint256[] memory _itemIds1 = new uint256[](3); + _itemIds1[0] = _tokenIds[0]; + _itemIds1[1] = _tokenIds[1]; + _itemIds1[2] = _tokenIds[2]; + + encodedItems1 = encode(address(itemBoundProxy), _itemIds1); + + uint256[] memory _itemIds2 = new uint256[](3); + _itemIds2[0] = _tokenIds[3]; + _itemIds2[1] = _tokenIds[4]; + _itemIds2[2] = _tokenIds[5]; + + encodedItems2 = encode(address(itemBoundProxy), _itemIds2); + + (nonce, signature) = generateSignature( + playerWallet.addr, + encodedItems1, + minterLabel + ); + (nonce2, signature2) = generateSignature( + playerWallet2.addr, + encodedItems2, + minterLabel + ); + } + + function testTokenExists() public { + uint256 _tokenId = generateRandomItemId(); + + vm.expectRevert(TokenNotExist.selector); + itemBoundProxy.isTokenExist(_tokenId); + + vm.expectRevert(TokenNotExist.selector); + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: string( + abi.encodePacked( + "https://something222.com", + "/", + _tokenId.toString() + ) + ), + receiver: address(0), + feeBasisPoints: 0 + }); + + itemBoundProxy.addNewToken(_token); + itemBoundProxy.isTokenExist(_tokenId); + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + } + + function testAddNewTokens() public { + LibItems.TokenCreate[] memory _tokens = new LibItems.TokenCreate[](3); + + skip(36000); + for (uint256 i = 0; i < 3; i++) { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: string( + abi.encodePacked( + "https://something.com", + "/", + _tokenId.toString() + ) + ), + receiver: address(0), + feeBasisPoints: 0 + }); + + _tokens[i] = _token; + } + + itemBoundProxy.addNewTokens(_tokens); + } + + function testPauseUnpause() public { + uint256 _tokenId = _tokenIds[0]; + + itemBoundProxy.pause(); + vm.expectRevert( + abi.encodeWithSelector(Pausable.EnforcedPause.selector) + ); + itemBoundProxy.adminMintId(address(this), _tokenId, 1, true); + itemBoundProxy.unpause(); + + itemBoundProxy.adminMintId( + address(mockERC1155Receiver), + _tokenId, + 1, + true + ); + assertEq( + itemBoundProxy.balanceOf(address(mockERC1155Receiver), _tokenId), + 1 + ); + } + + function testPauseUnpauseSpecificToken() public { + uint256 _tokenId = _tokenIds[0]; + + itemBoundProxy.updateTokenMintPaused(_tokenId, true); + + vm.expectRevert(TokenMintPaused.selector); + itemBoundProxy.adminMintId( + address(mockERC1155Receiver), + _tokenId, + 1, + true + ); + + vm.expectRevert(TokenMintPaused.selector); + itemBoundProxy.adminMint( + address(mockERC1155Receiver), + encodedItems1, + true + ); + + vm.expectRevert(TokenMintPaused.selector); + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + + itemBoundProxy.updateTokenMintPaused(_tokenId, false); + + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenId), 1); + } + + // testVerifySignature + function testInvalidSignature() public { + vm.prank(playerWallet.addr); + vm.expectRevert("InvalidSignature"); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature2); + } + + function testReuseSignatureMint() public { + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + vm.prank(playerWallet.addr); + vm.expectRevert("AlreadyUsedSignature"); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + } + + function testMintShouldPass() public { + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 1, + "" + ); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: can't be zero amount" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 0, + "" + ); + + vm.prank(playerWallet2.addr); + itemBoundProxy.mint(encodedItems2, 1, false, nonce2, signature2); + + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + minterWallet.addr, + _tokenIds[3], + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + assertEq(itemBoundProxy.balanceOf(playerWallet2.addr, _tokenIds[3]), 0); + assertEq(itemBoundProxy.balanceOf(minterWallet.addr, _tokenIds[3]), 1); + } + + function testMintMoreThanLimit() public { + vm.expectRevert(ExceedMaxMint.selector); + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 2, true, nonce, signature); + } + + function testMintInvalidTokenId() public { + uint256[] memory _itemIds3 = new uint256[](3); + _itemIds3[0] = 1233; + _itemIds3[1] = 3322; + + bytes memory encodedItems3 = encode(address(itemBoundProxy), _itemIds3); + + (uint256 _nonce, bytes memory _signature) = generateSignature( + playerWallet.addr, + encodedItems3, + minterLabel + ); + + vm.expectRevert(TokenNotExist.selector); + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems3, 1, true, _nonce, _signature); + } + + function testAdminMintNotMinterRole() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + playerWallet.addr, + MINTER_ROLE + ) + ); + vm.prank(playerWallet.addr); + itemBoundProxy.adminMint(playerWallet.addr, encodedItems1, true); + } + + function testAdminMint() public { + itemBoundProxy.adminMint( + address(mockERC1155Receiver), + encodedItems1, + true + ); + assertEq( + itemBoundProxy.balanceOf( + address(mockERC1155Receiver), + _tokenIds[0] + ), + 1 + ); + assertEq( + itemBoundProxy.balanceOf( + address(mockERC1155Receiver), + _tokenIds[1] + ), + 1 + ); + assertEq( + itemBoundProxy.balanceOf( + address(mockERC1155Receiver), + _tokenIds[2] + ), + 1 + ); + } + + function testAdminMintIdNotMinterRole() public { + uint256 _tokenId = _tokenIds[0]; + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + playerWallet.addr, + MINTER_ROLE + ) + ); + vm.prank(playerWallet.addr); + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + } + + function testAdminMintId() public { + uint256 _tokenId = _tokenIds[0]; + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + } + + function testAdminBatchMintById() public { + uint256[] memory _itemIds = new uint256[](3); + _itemIds[0] = _tokenIds[0]; + _itemIds[1] = _tokenIds[1]; + _itemIds[2] = _tokenIds[2]; + + uint256[] memory _amount = new uint256[](3); + _amount[0] = 1; + _amount[1] = 1; + _amount[2] = 1; + + itemBoundProxy.adminBatchMintById( + playerWallet.addr, + _itemIds, + _amount, + true + ); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _itemIds[0]), 1); + } + + function testBurnNotOwnerShouldFail() public { + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, false, nonce, signature); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Errors.ERC1155MissingApprovalForAll.selector, + playerWallet2.addr, + playerWallet.addr + ) + ); + vm.prank(playerWallet2.addr); + itemBoundProxy.burn(playerWallet.addr, _tokenIds[0], 1); + } + + function testBurn() public { + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 1, + "" + ); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.burn(playerWallet.addr, _tokenIds[0], 1); + + vm.prank(playerWallet2.addr); + itemBoundProxy.mint(encodedItems2, 1, false, nonce2, signature2); + + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet3.addr, + _tokenIds[3], + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet2.addr, _tokenIds[3]), 0); + assertEq(itemBoundProxy.balanceOf(playerWallet3.addr, _tokenIds[3]), 1); + + vm.prank(playerWallet3.addr); + itemBoundProxy.burn(playerWallet3.addr, _tokenIds[3], 1); + + assertEq(itemBoundProxy.balanceOf(playerWallet3.addr, _tokenIds[3]), 0); + } + + function testBurnIfHoldBothNonSoulboundAndSouldbound() public { + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + + itemBoundProxy.adminMint(playerWallet2.addr, encodedItems1, false); + + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet.addr, + _tokenIds[0], + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 2); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 2, + "" + ); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 1, + "" + ); + } + + function testBurnBatchNotOwnerShouldFail() public { + uint256[] memory _itemIds1 = new uint256[](3); + _itemIds1[0] = _tokenIds[0]; + _itemIds1[1] = _tokenIds[1]; + _itemIds1[2] = _tokenIds[2]; + + uint256[] memory _amount1 = new uint256[](3); + _amount1[0] = 1; + _amount1[1] = 1; + _amount1[2] = 1; + + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, false, nonce, signature); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Errors.ERC1155MissingApprovalForAll.selector, + playerWallet2.addr, + playerWallet.addr + ) + ); + vm.prank(playerWallet2.addr); + itemBoundProxy.burnBatch(playerWallet.addr, _itemIds1, _amount1); + } + + function testBurnBatch() public { + uint256[] memory _itemIds1 = new uint256[](3); + _itemIds1[0] = _tokenIds[0]; + _itemIds1[1] = _tokenIds[1]; + _itemIds1[2] = _tokenIds[2]; + + uint256[] memory _itemIds2 = new uint256[](3); + _itemIds2[0] = _tokenIds[3]; + _itemIds2[1] = _tokenIds[4]; + _itemIds2[2] = _tokenIds[5]; + + uint256[] memory _amount1 = new uint256[](3); + _amount1[0] = 1; + _amount1[1] = 1; + _amount1[2] = 1; + + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 1, + "" + ); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.burnBatch(playerWallet.addr, _itemIds1, _amount1); + + vm.prank(playerWallet2.addr); + itemBoundProxy.mint(encodedItems2, 1, false, nonce2, signature2); + + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet3.addr, + _tokenIds[3], + 1, + "" + ); + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet3.addr, + _tokenIds[4], + 1, + "" + ); + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet3.addr, + _tokenIds[5], + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet2.addr, _tokenIds[3]), 0); + assertEq(itemBoundProxy.balanceOf(playerWallet3.addr, _tokenIds[3]), 1); + + vm.prank(playerWallet3.addr); + itemBoundProxy.burnBatch(playerWallet3.addr, _itemIds2, _amount1); + + assertEq(itemBoundProxy.balanceOf(playerWallet3.addr, _tokenIds[3]), 0); + } + + function testBatchTransferFrom() public { + uint256[] memory _itemIds1 = new uint256[](3); + _itemIds1[0] = _tokenIds[0]; + _itemIds1[1] = _tokenIds[1]; + _itemIds1[2] = _tokenIds[2]; + + uint256[] memory _itemIds2 = new uint256[](3); + _itemIds2[0] = _tokenIds[3]; + _itemIds2[1] = _tokenIds[4]; + _itemIds2[2] = _tokenIds[5]; + + uint256[] memory _amount1 = new uint256[](3); + _amount1[0] = 1; + _amount1[1] = 1; + _amount1[2] = 1; + + vm.prank(playerWallet.addr); + itemBoundProxy.mint(encodedItems1, 1, true, nonce, signature); + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 1); + + itemBoundProxy.adminMint(playerWallet2.addr, encodedItems1, false); + + vm.prank(playerWallet2.addr); + itemBoundProxy.safeTransferFrom( + playerWallet2.addr, + playerWallet.addr, + _tokenIds[0], + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenIds[0]), 2); + + uint256[] memory _itemIds3 = new uint256[](2); + _itemIds3[0] = _tokenIds[0]; + _itemIds3[1] = _tokenIds[0]; + + uint256[] memory _amount3 = new uint256[](2); + _amount3[0] = 1; + _amount3[1] = 1; + + vm.expectRevert(DuplicateID.selector); + vm.prank(playerWallet.addr); + itemBoundProxy.safeBatchTransferFrom( + playerWallet.addr, + minterWallet.addr, + _itemIds3, + _amount3, + "" + ); + + assertEq(itemBoundProxy.balanceOf(minterWallet.addr, _tokenIds[0]), 0); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[0], + 1, + "" + ); + assertEq(itemBoundProxy.balanceOf(minterWallet.addr, _tokenIds[0]), 1); + } + + function testTokenURIIfTokenIdNotExist() public { + vm.expectRevert(TokenNotExist.selector); + itemBoundProxy.uri(1); + } + + function testTokenURIIfTokenIdExistNOSpeficTokenURIFallbackToBaseURI() + public + { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: "", + receiver: address(0), + feeBasisPoints: 0 + }); + + itemBoundProxy.addNewToken(_token); + + assertEq( + itemBoundProxy.uri(_tokenId), + string( + abi.encodePacked("MISSING_BASE_URL", "/", _tokenId.toString()) + ) + ); + } + + function testTokenURIIfTokenIdExistWithSpeficTokenURI() public { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: "ipfs://specific-token-uri.com", + receiver: address(0), + feeBasisPoints: 0 + }); + + itemBoundProxy.addNewToken(_token); + + assertEq(itemBoundProxy.uri(_tokenId), "ipfs://specific-token-uri.com"); + } + + function testUpdateTokenBaseURIFailNotManagerRole() public { + string memory newBaseURI = "https://something-new.com"; + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + playerWallet.addr, + DEV_CONFIG_ROLE + ) + ); + vm.prank(playerWallet.addr); + itemBoundProxy.updateBaseUri(newBaseURI); + } + + function testUpdateTokenBaseURIPass() public { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: "", + receiver: address(0), + feeBasisPoints: 0 + }); + + itemBoundProxy.addNewToken(_token); + + string memory newBaseURI = "https://something-new.com"; + + assertEq( + itemBoundProxy.uri(_tokenId), + string( + abi.encodePacked("MISSING_BASE_URL", "/", _tokenId.toString()) + ) + ); + itemBoundProxy.updateBaseUri(newBaseURI); + assertEq( + itemBoundProxy.uri(_tokenId), + string( + abi.encodePacked( + "https://something-new.com", + "/", + _tokenId.toString() + ) + ) + ); + } + + function testUpdateTokenURIFailNotManagerRole() public { + string memory newTokenUri = "https://something-new.com/232"; + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + playerWallet.addr, + DEV_CONFIG_ROLE + ) + ); + vm.prank(playerWallet.addr); + itemBoundProxy.updateTokenUri(0, newTokenUri); + } + + function testUpdateTokenURIPass() public { + uint256 _tokenId = generateRandomItemId(); // totally random + uint256 _level = generateRandomLevel(); // level 1-10 + TestLibItems.Tier _tier = generateRandomTier(); // tier 0-4 + + LibItems.TokenCreate memory _token = LibItems.TokenCreate({ + tokenId: _tokenId, + tokenUri: "", + receiver: address(0), + feeBasisPoints: 0 + }); + + itemBoundProxy.addNewToken(_token); + + string memory newTokenUri = "https://something-new.com/232"; + + assertEq( + itemBoundProxy.uri(_tokenId), + string( + abi.encodePacked("MISSING_BASE_URL", "/", _tokenId.toString()) + ) + ); + itemBoundProxy.updateTokenUri(_tokenId, newTokenUri); + assertEq(itemBoundProxy.uri(_tokenId), "https://something-new.com/232"); + } + + function testNonSoulboundTokenTransfer() public { + uint256 _tokenId = _tokenIds[0]; + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, false); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenId, + 1, + "" + ); + + assertEq(itemBoundProxy.balanceOf(playerWallet.addr, _tokenId), 0); + assertEq(itemBoundProxy.balanceOf(minterWallet.addr, _tokenId), 1); + } + + function testSoulboundTokenNotTransfer() public { + uint256 _tokenId = _tokenIds[0]; + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenId, + 1, + "" + ); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: can't be zero amount" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenId, + 0, + "" + ); + } + + function testSoulboundTokenTransferOnlyWhitelistAddresses() public { + uint256 _tokenId = _tokenIds[0]; + itemBoundProxy.adminMintId(playerWallet.addr, _tokenId, 1, true); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + playerWallet3.addr, + _tokenId, + 1, + "" + ); + + itemBoundProxy.updateWhitelistAddress(playerWallet3.addr, true); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + playerWallet3.addr, + _tokenId, + 1, + "" + ); + + vm.prank(playerWallet3.addr); + itemBoundProxy.safeTransferFrom( + playerWallet3.addr, + playerWallet.addr, + _tokenId, + 1, + "" + ); + + itemBoundProxy.updateWhitelistAddress(playerWallet3.addr, false); + + vm.expectRevert( + "Achievo1155SoulboundUpgradeable: The amount of soulbounded tokens is more than the amount of tokens to be transferred" + ); + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + playerWallet3.addr, + _tokenId, + 1, + "" + ); + } + + function testTokenRoyaltyDefault() public { + uint256 mintPrice = 1 ether; + uint256 expectedRoyalty = (mintPrice * 250) / 10000; + + (address receiver, uint256 royaltyAmount) = itemBoundProxy.royaltyInfo( + 1, + mintPrice + ); + + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + } + + function testSetTokenRoyaltyAndReset() public { + uint256 mintPrice = 1 ether; + uint256 tokenId = 1; + + (address receiver, uint256 royaltyAmount) = itemBoundProxy.royaltyInfo( + tokenId, + mintPrice + ); + + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + + uint256 expectedRoyaltyAfter = (mintPrice * 300) / 10000; + itemBoundProxy.setTokenRoyalty(tokenId, playerWallet.addr, 300); + + (address receiverAfter, uint256 royaltyAmountAfter) = itemBoundProxy + .royaltyInfo(tokenId, mintPrice); + + assertEq(receiverAfter, playerWallet.addr); + assertEq(royaltyAmountAfter, expectedRoyaltyAfter); + + itemBoundProxy.resetTokenRoyalty(tokenId); + + ( + address receiverAfterReset, + uint256 royaltyAmountAfterReset + ) = itemBoundProxy.royaltyInfo(tokenId, mintPrice); + assertEq(receiverAfterReset, address(0)); + assertEq(royaltyAmountAfterReset, 0); + } + + function testSetTokenRoyaltyShouldFailNotManagerRole() public { + uint256 tokenId = 1; + + vm.prank(minterWallet.addr); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + minterWallet.addr, + MANAGER_ROLE + ) + ); + itemBoundProxy.setTokenRoyalty(tokenId, playerWallet.addr, 300); + + vm.prank(playerWallet.addr); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + playerWallet.addr, + MANAGER_ROLE + ) + ); + itemBoundProxy.setTokenRoyalty(tokenId, playerWallet.addr, 300); + + itemBoundProxy.grantRole( + itemBoundProxy.MANAGER_ROLE(), + playerWallet.addr + ); + + vm.prank(playerWallet.addr); + itemBoundProxy.setTokenRoyalty(tokenId, playerWallet.addr, 300); + + uint256 mintPrice = 1 ether; + uint256 expectedRoyaltyAfter = (mintPrice * 300) / 10000; + + (address receiverAfter, uint256 royaltyAmountAfter) = itemBoundProxy + .royaltyInfo(tokenId, mintPrice); + + assertEq(receiverAfter, playerWallet.addr); + assertEq(royaltyAmountAfter, expectedRoyaltyAfter); + } + + function testgetAllItems() public { + bytes memory encodedItemsAll = encode( + address(itemBoundProxy), + _tokenIds + ); + itemBoundProxy.adminMint(playerWallet.addr, encodedItemsAll, false); + + string memory newTokenUri = "https://something-new.com/232"; + itemBoundProxy.updateTokenUri(_tokenIds[23], newTokenUri); + assertEq( + itemBoundProxy.uri(_tokenIds[23]), + "https://something-new.com/232" + ); + + vm.prank(playerWallet.addr); + LibItems.TokenReturn[] memory allTokensInfo = itemBoundProxy + .getAllItems(playerWallet.addr); + assertEq(allTokensInfo.length, 1300); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[24], + 1, + "" + ); + + vm.prank(playerWallet.addr); + LibItems.TokenReturn[] memory allTokensInfo2 = itemBoundProxy + .getAllItems(playerWallet.addr); + assertEq(allTokensInfo2.length, 1299); + + for (uint256 i = 0; i < allTokensInfo.length; i++) { + assertEq(allTokensInfo[i].tokenId, _tokenIds[i]); + + if (i == 23) { + assertEq(allTokensInfo[i].tokenUri, newTokenUri); + assertEq(allTokensInfo[i].amount, 1); + } else { + assertEq(allTokensInfo[i].amount, 1); + assertEq( + allTokensInfo[i].tokenUri, + string( + abi.encodePacked( + "https://something.com", + "/", + _tokenIds[i].toString() + ) + ) + ); + } + } + + vm.prank(minterWallet.addr); + LibItems.TokenReturn[] memory allTokensInfo3 = itemBoundProxy + .getAllItems(minterWallet.addr); + assertEq(allTokensInfo3.length, 1); + } + + function testgetAllItemsAdmin() public { + bytes memory encodedItemsAll = encode( + address(itemBoundProxy), + _tokenIds + ); + itemBoundProxy.adminMint(playerWallet.addr, encodedItemsAll, false); + + string memory newTokenUri = "https://something-new.com/232"; + itemBoundProxy.updateTokenUri(_tokenIds[23], newTokenUri); + assertEq( + itemBoundProxy.uri(_tokenIds[23]), + "https://something-new.com/232" + ); + + LibItems.TokenReturn[] memory allTokensInfo = itemBoundProxy + .getAllItems(playerWallet.addr); + assertEq(allTokensInfo.length, 1300); + + vm.prank(playerWallet.addr); + itemBoundProxy.safeTransferFrom( + playerWallet.addr, + minterWallet.addr, + _tokenIds[24], + 1, + "" + ); + + LibItems.TokenReturn[] memory allTokensInfo2 = itemBoundProxy + .getAllItems(playerWallet.addr); + assertEq(allTokensInfo2.length, 1300); + + for (uint256 i = 0; i < allTokensInfo.length; i++) { + assertEq(allTokensInfo[i].tokenId, _tokenIds[i]); + + if (i == 23) { + assertEq(allTokensInfo[i].tokenUri, newTokenUri); + assertEq(allTokensInfo[i].amount, 1); + } else { + assertEq(allTokensInfo[i].amount, 1); + assertEq( + allTokensInfo[i].tokenUri, + string( + abi.encodePacked( + "https://something.com", + "/", + _tokenIds[i].toString() + ) + ) + ); + } + } + + LibItems.TokenReturn[] memory allTokensInfo3 = itemBoundProxy + .getAllItems(minterWallet.addr); + assertEq(allTokensInfo3.length, 1300); + } +} diff --git a/test/hardhatTests/ERC1155RoyaltiesSoulbound.test.ts b/test/hardhatTests/ERC1155RoyaltiesSoulbound.test.ts new file mode 100644 index 0000000..6a85dd2 --- /dev/null +++ b/test/hardhatTests/ERC1155RoyaltiesSoulbound.test.ts @@ -0,0 +1,231 @@ +import { ERC1155RoyaltiesSoulboundV3 } from '../../typechain-types'; + +const { expect } = require('chai'); +const { ethers, upgrades } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +describe.only('ERC1155RoyaltiesSoulboundV3', function () { + async function deployFixtures() { + const [devWallet, minterWallet, user1, user2] = await ethers.getSigners(); + + const ERC1155RoyaltiesSoulboundV3Factory = await ethers.getContractFactory('ERC1155RoyaltiesSoulboundV3'); + const erc1155RoyaltiesSoulbound = (await upgrades.deployProxy( + ERC1155RoyaltiesSoulboundV3Factory, + [ + 'G7Reward', + 'G7R', + 'https://example.com/token/', + 'https://example.com/contract/', + 1, + false, + devWallet.address, + ], + { + initializer: 'initialize', + } + )) as ERC1155RoyaltiesSoulboundV3; + + return { + erc1155RoyaltiesSoulbound, + devWallet, + minterWallet, + user1, + user2, + }; + } + + describe('Initialization', function () { + it('Should deploy successfully', async function () { + const { erc1155RoyaltiesSoulbound } = await loadFixture(deployFixtures); + expect(await erc1155RoyaltiesSoulbound.getAddress()).to.be.properAddress; + }); + it('should set the correct name and symbol', async function () { + const { erc1155RoyaltiesSoulbound } = await loadFixture(deployFixtures); + expect(await erc1155RoyaltiesSoulbound.name()).to.equal('G7Reward'); + expect(await erc1155RoyaltiesSoulbound.symbol()).to.equal('G7R'); + }); + + it('should set the correct contract URI', async function () { + const { erc1155RoyaltiesSoulbound } = await loadFixture(deployFixtures); + expect(await erc1155RoyaltiesSoulbound.contractURI()).to.equal('https://example.com/contract/'); + }); + }); + + describe('Role management', function () { + it('should grant the correct roles', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet } = await loadFixture(deployFixtures); + expect( + await erc1155RoyaltiesSoulbound.hasRole( + await erc1155RoyaltiesSoulbound.DEFAULT_ADMIN_ROLE(), + devWallet.address + ) + ).to.be.true; + expect( + await erc1155RoyaltiesSoulbound.hasRole( + await erc1155RoyaltiesSoulbound.MANAGER_ROLE(), + devWallet.address + ) + ).to.be.true; + expect( + await erc1155RoyaltiesSoulbound.hasRole( + await erc1155RoyaltiesSoulbound.DEV_CONFIG_ROLE(), + devWallet.address + ) + ).to.be.true; + expect( + await erc1155RoyaltiesSoulbound.hasRole( + await erc1155RoyaltiesSoulbound.DEV_CONFIG_ROLE(), + minterWallet.address + ) + ).to.be.true; + expect( + await erc1155RoyaltiesSoulbound.hasRole( + await erc1155RoyaltiesSoulbound.MINTER_ROLE(), + minterWallet.address + ) + ).to.be.true; + }); + }); + + describe('Token management', function () { + it('should add a new token', async function () { + const { erc1155RoyaltiesSoulbound, devWallet } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + expect(await erc1155RoyaltiesSoulbound.isTokenExist(1)).to.be.true; + }); + + it('should revert when querying a non-existent token', async function () { + const { erc1155RoyaltiesSoulbound } = await loadFixture(deployFixtures); + await expect(erc1155RoyaltiesSoulbound.isTokenExist(999)).to.be.revertedWith('TokenNotExist'); + }); + }); + + describe('Minting', function () { + it('should mint a token', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 1, false); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(1); + }); + + it('should mint a soulbound token', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 1, true); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(1); + await expect( + erc1155RoyaltiesSoulbound.connect(user1).safeTransferFrom(user1.address, devWallet.address, 1, 1, '0x') + ).to.be.revertedWithCustomError(erc1155RoyaltiesSoulbound, 'SoulboundAmountError'); + }); + + it('should batch mint tokens', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1, user2 } = + await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound + .connect(minterWallet) + .adminBatchMintId([user1.address, user2.address], 1, [1, 2], false); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(1); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user2.address, 1)).to.equal(2); + }); + }); + + describe('Token transfers', function () { + it('should allow transfer of non-soulbound tokens', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1, user2 } = + await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 20, false); + await erc1155RoyaltiesSoulbound.connect(user1).safeTransferFrom(user1.address, user2.address, 1, 5, '0x'); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(15); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user2.address, 1)).to.equal(5); + }); + + it('should not allow transfer of soulbound tokens', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1, user2 } = + await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 1, true); + await expect( + erc1155RoyaltiesSoulbound.connect(user1).safeTransferFrom(user1.address, user2.address, 1, 1, '0x') + ).to.be.revertedWithCustomError(erc1155RoyaltiesSoulbound, 'SoulboundAmountError'); + }); + }); + + describe('Burning', function () { + it('should allow burning of non-soulbound tokens', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 2, false); + await erc1155RoyaltiesSoulbound.connect(user1).burn(user1.address, 1, 1); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(1); + }); + + it('should not allow burning of soulbound tokens without being in the burn whitelist', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 1, true); + await expect( + erc1155RoyaltiesSoulbound.connect(user1).burn(user1.address, 1, 1) + ).to.be.revertedWithCustomError(erc1155RoyaltiesSoulbound, 'SoulboundAmountError'); + }); + + it('should allow whitelisted address to burn tokens', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, minterWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(minterWallet).adminMintId(user1.address, 1, 2, false); + await erc1155RoyaltiesSoulbound.connect(devWallet).updateWhitelistAddress(devWallet.address, true); + await erc1155RoyaltiesSoulbound.connect(devWallet).whitelistBurn(user1.address, 1, 1); + expect(await erc1155RoyaltiesSoulbound.balanceOf(user1.address, 1)).to.equal(1); + }); + }); + + describe('URI management', function () { + it('should return the correct token URI', async function () { + const { erc1155RoyaltiesSoulbound, devWallet } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + expect(await erc1155RoyaltiesSoulbound.uri(1)).to.equal('https://example.com/token/'); + }); + + it('should update the default token URI', async function () { + const { erc1155RoyaltiesSoulbound, devWallet } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).updateDefaultTokenURI('https://newexample.com/token/'); + expect(await erc1155RoyaltiesSoulbound.defaultTokenURI()).to.equal('https://newexample.com/token/'); + }); + + it('should update the contract URI', async function () { + const { erc1155RoyaltiesSoulbound, devWallet } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).setContractURI('https://newexample.com/contract'); + expect(await erc1155RoyaltiesSoulbound.contractURI()).to.equal('https://newexample.com/contract'); + }); + }); + + describe('Royalty management', function () { + it('should set default royalty info', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, user1 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).setRoyaltyInfo(user1.address, 500); // 5% + const [receiver, royaltyAmount] = await erc1155RoyaltiesSoulbound.royaltyInfo(1, 10000); + expect(receiver).to.equal(user1.address); + expect(royaltyAmount).to.equal(500); + }); + + it('should set token-specific royalty', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, user2 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(devWallet).setTokenRoyalty(1, user2.address, 1000); // 10% + const [receiver, royaltyAmount] = await erc1155RoyaltiesSoulbound.royaltyInfo(1, 10000); + expect(receiver).to.equal(user2.address); + expect(royaltyAmount).to.equal(1000); + }); + + it('should reset token-specific royalty', async function () { + const { erc1155RoyaltiesSoulbound, devWallet, user2 } = await loadFixture(deployFixtures); + await erc1155RoyaltiesSoulbound.connect(devWallet).addNewToken(1); + await erc1155RoyaltiesSoulbound.connect(devWallet).setTokenRoyalty(1, user2.address, 1000); + await erc1155RoyaltiesSoulbound.connect(devWallet).resetTokenRoyalty(1); + const [receiver, royaltyAmount] = await erc1155RoyaltiesSoulbound.royaltyInfo(1, 10000); + expect(receiver).to.equal(ethers.ZeroAddress); + expect(royaltyAmount).to.equal(0); + }); + }); +});