diff --git a/contracts/FeralfileArtworkV3_2.sol b/contracts/FeralfileArtworkV3_2.sol new file mode 100644 index 0000000..74814f3 --- /dev/null +++ b/contracts/FeralfileArtworkV3_2.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/Base64.sol"; + +import "./Authorizable.sol"; +import "./FeralfileArtworkV3.sol"; +import "./UpdateableOperatorFilterer.sol"; + +contract FeralfileExhibitionV3_2 is + FeralfileExhibitionV3, + UpdateableOperatorFilterer +{ + constructor( + string memory name_, + string memory symbol_, + uint256 secondarySaleRoyaltyBPS_, + address royaltyPayoutAddress_, + string memory contractURI_, + string memory tokenBaseURI_, + bool isBurnable_, + bool isBridgeable_ + ) + FeralfileExhibitionV3( + name_, + symbol_, + secondarySaleRoyaltyBPS_, + royaltyPayoutAddress_, + contractURI_, + tokenBaseURI_, + isBurnable_, + isBridgeable_ + ) + {} + + function setApprovalForAll(address operator, bool approved) + public + override(ERC721, IERC721) + onlyAllowedOperatorApproval(operator) + { + super.setApprovalForAll(operator, approved); + } + + function approve(address operator, uint256 tokenId) + public + override(ERC721, IERC721) + onlyAllowedOperatorApproval(operator) + { + super.approve(operator, tokenId); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public override(ERC721, IERC721) onlyAllowedOperator(from) { + super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public override(ERC721, IERC721) onlyAllowedOperator(from) { + super.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public override(ERC721, IERC721) onlyAllowedOperator(from) { + super.safeTransferFrom(from, to, tokenId, data); + } +} diff --git a/contracts/UpdateableOperatorFilterer.sol b/contracts/UpdateableOperatorFilterer.sol new file mode 100644 index 0000000..754efbe --- /dev/null +++ b/contracts/UpdateableOperatorFilterer.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IOperatorFilterRegistry} from "./operator-filter-registry/IOperatorFilterRegistry.sol"; + +import "./Authorizable.sol"; + +/** + * @title UpdateableOperatorFilterer + * @notice Abstract contract whose constructor automatically registers and optionally subscribes to or copies another + * registrant's entries in the OperatorFilterRegistry. + * @dev This smart contract is meant to be inherited by token contracts so they can use the following: + * - `onlyAllowedOperator` modifier for `transferFrom` and `safeTransferFrom` methods. + * - `onlyAllowedOperatorApproval` modifier for `approve` and `setApprovalForAll` methods. + */ +abstract contract UpdateableOperatorFilterer is Authorizable { + error OperatorNotAllowed(address operator); + + address constant DEFAULT_OPERATOR_FILTER_REGISTRY_ADDRESS = + address(0x000000000000AAeB6D7670E522A718067333cd4E); + + address constant DEFAULT_SUBSCRIPTION = + address(0x3cc6CddA760b79bAfa08dF41ECFA224f810dCeB6); + + IOperatorFilterRegistry public OperatorFilterRegistry = + IOperatorFilterRegistry(DEFAULT_OPERATOR_FILTER_REGISTRY_ADDRESS); + + constructor() { + if (address(OperatorFilterRegistry).code.length > 0) { + OperatorFilterRegistry.registerAndSubscribe( + address(this), + DEFAULT_SUBSCRIPTION + ); + } + } + + modifier onlyAllowedOperator(address from) virtual { + // Allow spending tokens from addresses with balance + // Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred + // from an EOA. + if (from != msg.sender) { + _checkFilterOperator(msg.sender); + } + _; + } + + modifier onlyAllowedOperatorApproval(address operator) virtual { + _checkFilterOperator(operator); + _; + } + + function _checkFilterOperator(address operator) internal view virtual { + // Check registry code length to facilitate testing in environments without a deployed registry. + if (address(OperatorFilterRegistry).code.length > 0) { + require( + OperatorFilterRegistry.isOperatorAllowed( + address(this), + operator + ), + "operator is not allowed" + ); + } + } + + /** + * @notice update the operator filter registry + */ + function updateOperatorFilterRegistry(address operatorFilterRegisterAddress) + external + onlyOwner + { + OperatorFilterRegistry = IOperatorFilterRegistry( + operatorFilterRegisterAddress + ); + } +} diff --git a/contracts/mocks/MockOperatorFilterRegistry.sol b/contracts/mocks/MockOperatorFilterRegistry.sol new file mode 100644 index 0000000..e12af4e --- /dev/null +++ b/contracts/mocks/MockOperatorFilterRegistry.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MockOperatorFilterRegistry { + address private allowedExchange; + + constructor(address allowedExchange_) { + allowedExchange = allowedExchange_; + } + + function isOperatorAllowed(address registrant, address operator) + external + view + returns (bool) + { + // allow for all regular address + if (operator.code.length == 0) { + return true; + } + + if (operator == allowedExchange) { + return true; + } + + return false; + } +} diff --git a/contracts/operator-filter-registry/IOperatorFilterRegistry.sol b/contracts/operator-filter-registry/IOperatorFilterRegistry.sol new file mode 100644 index 0000000..f26750b --- /dev/null +++ b/contracts/operator-filter-registry/IOperatorFilterRegistry.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface IOperatorFilterRegistry { + function isOperatorAllowed(address registrant, address operator) external view returns (bool); + function register(address registrant) external; + function registerAndSubscribe(address registrant, address subscription) external; + function registerAndCopyEntries(address registrant, address registrantToCopy) external; + function unregister(address addr) external; + function updateOperator(address registrant, address operator, bool filtered) external; + function updateOperators(address registrant, address[] calldata operators, bool filtered) external; + function updateCodeHash(address registrant, bytes32 codehash, bool filtered) external; + function updateCodeHashes(address registrant, bytes32[] calldata codeHashes, bool filtered) external; + function subscribe(address registrant, address registrantToSubscribe) external; + function unsubscribe(address registrant, bool copyExistingEntries) external; + function subscriptionOf(address addr) external returns (address registrant); + function subscribers(address registrant) external returns (address[] memory); + function subscriberAt(address registrant, uint256 index) external returns (address); + function copyEntriesOf(address registrant, address registrantToCopy) external; + function isOperatorFiltered(address registrant, address operator) external returns (bool); + function isCodeHashOfFiltered(address registrant, address operatorWithCode) external returns (bool); + function isCodeHashFiltered(address registrant, bytes32 codeHash) external returns (bool); + function filteredOperators(address addr) external returns (address[] memory); + function filteredCodeHashes(address addr) external returns (bytes32[] memory); + function filteredOperatorAt(address registrant, uint256 index) external returns (address); + function filteredCodeHashAt(address registrant, uint256 index) external returns (bytes32); + function isRegistered(address addr) external returns (bool); + function codeHashOf(address addr) external returns (bytes32); +} diff --git a/migrations/203_feralfile_exhibition_v3_2.js b/migrations/203_feralfile_exhibition_v3_2.js new file mode 100644 index 0000000..dbab3eb --- /dev/null +++ b/migrations/203_feralfile_exhibition_v3_2.js @@ -0,0 +1,29 @@ +var FeralfileExhibitionV3_2 = artifacts.require('FeralfileExhibitionV3_2'); + +const argv = require('minimist')(process.argv.slice(2), { + string: ['exhibition_curator'], +}); + +module.exports = function (deployer) { + let exhibition_name = argv.exhibition_name || 'Feral File V3_2 Test'; + let exhibition_symbol = argv.exhibition_symbol || 'FFDV3_2'; + let exhibition_royalty_payout_address = + argv.exhibition_royalty_payout_address || + '0xa704AA575172F84D917CcaF5EC775Da8227Ea6c3'; + let exhibition_secondary_sale_royalty_bps = + argv.exhibition_secondary_sale_royalty_bps || 1000; + let burnable = argv.burnable || true; + let bridgeable = argv.bridgeable || true; + + deployer.deploy( + FeralfileExhibitionV3_2, + exhibition_name, + exhibition_symbol, + exhibition_secondary_sale_royalty_bps, + exhibition_royalty_payout_address, + 'https://ipfs.bitmark.com/ipfs/QmaQegRqExfFx8zuR6yscxzUwQJUc96LuNNQiAMK9BsUQe', + 'https://ipfs.bitmark.com/ipfs/', + burnable, + bridgeable + ); +}; diff --git a/test/feralfile_exhibition_v3_2.js b/test/feralfile_exhibition_v3_2.js new file mode 100644 index 0000000..0577ca3 --- /dev/null +++ b/test/feralfile_exhibition_v3_2.js @@ -0,0 +1,139 @@ +const FeralfileExhibitionV3_2 = artifacts.require('FeralfileExhibitionV3_2'); +const MockOperatorFilterRegistry = artifacts.require('MockOperatorFilterRegistry'); + +const axios = require('axios'); + +const IPFS_GATEWAY_PREFIX = 'https://ipfs.bitmark.com/ipfs/'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const ALLOWED_EXCHANGE = '0x2de783974f53AC98b16332f943431df0FD66C9E4'; + +contract('FeralfileExhibitionV3_2', async (accounts) => { + before(async function () { + this.operatorFilterRegistry = await MockOperatorFilterRegistry.new(ALLOWED_EXCHANGE) + this.testThumbnailCid = "QmV68mphFwMraCE9J6KpQc89Sz8ppvJx5CP6XFruhGQrX8" // AKG test IPFS thumbnail CID + this.testArtworkCid = "QmTn3PfHHvoDHKawTPXutqxAk2k8ynFK9cZfsSwggryjkX" // AKG test IPFS artwork CID + + this.exhibition = await FeralfileExhibitionV3_2.new( + 'Feral File V3_2 Test 002', + 'FFV3', + 1000, + '0x8fd310de32848798eB64Bd88f9C5656Eea32415e', + 'https://ipfs.bitmark.com/ipfs/QmaptARVxNSP36PQai5oiCPqbrATvpydcJ8SPx6T6Yp1CZ', + IPFS_GATEWAY_PREFIX, + true, + true + ); + + await this.exhibition.updateOperatorFilterRegistry(this.operatorFilterRegistry.address) + }); + + it('can register artwork', async function () { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', 'TestUser', 'TestArtwork', 10, 2, 1], + ]); + this.artworkID = r.logs[0].args.artworkID; + + let artwork = await this.exhibition.artworks(this.artworkID); + assert.equal(artwork.title, 'TestArtwork'); + + let artworkEditions = await this.exhibition.totalEditionOfArtwork( + this.artworkID + ); + assert.equal(artworkEditions, 0); + }); + + it('test batch mint 1 artwork', async function () { + let editionIndex0 = 0; + let editionIndex1 = 1; + let editionIndex2 = 2; + let editionIndex3 = 3; + + await this.exhibition.batchMint([ + [this.artworkID, editionIndex0, accounts[0], accounts[0], 'test0'], + [this.artworkID, editionIndex1, accounts[0], accounts[0], 'test1'], + [this.artworkID, editionIndex2, accounts[0], accounts[0], 'test2'], + [this.artworkID, editionIndex3, accounts[0], accounts[0], 'test3'], + ]); + + let account0 = accounts[0].toLowerCase() + + this.editionID0 = (BigInt(this.artworkID) + BigInt(editionIndex0)).toString(); + let ownerOfEdition0 = await this.exhibition.ownerOf(this.editionID0); + assert.equal(ownerOfEdition0.toLowerCase(), account0); + + this.editionID1 = (BigInt(this.artworkID) + BigInt(editionIndex1)).toString(); + let ownerOfEdition1 = await this.exhibition.ownerOf(this.editionID1); + assert.equal(ownerOfEdition1.toLowerCase(), account0); + + this.editionID2 = (BigInt(this.artworkID) + BigInt(editionIndex2)).toString(); + let ownerOfEdition2 = await this.exhibition.ownerOf(this.editionID2); + assert.equal(ownerOfEdition2.toLowerCase(), account0); + + this.editionID3 = (BigInt(this.artworkID) + BigInt(editionIndex3)).toString(); + let ownerOfEdition3 = await this.exhibition.ownerOf(this.editionID3); + assert.equal(ownerOfEdition3.toLowerCase(), account0); + }) + + it('can not approve an filtered exchange', async function () { + let onError = false; + try { + await this.exhibition.approve(this.operatorFilterRegistry.address, this.editionID0) + } catch (error) { + onError = true + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert operator is not allowed' + ); + } + assert.equal(onError, true) + let addr = await this.exhibition.getApproved(this.editionID0) + assert.equal(addr, ZERO_ADDRESS) + }) + + it('can approve the allowed exchange', async function () { + await this.exhibition.approve(ALLOWED_EXCHANGE, this.editionID0) + let addr = await this.exhibition.getApproved(this.editionID0) + assert.equal(addr, ALLOWED_EXCHANGE) + }) + + + it('can not transfer before approve', async function () { + let onError = false; + try { + await this.exhibition.safeTransferFrom(accounts[0], accounts[1], this.editionID1, { + "from": accounts[1] + }) + } catch (error) { + onError = true + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert ERC721: caller is not token owner nor approved' + ); + } + assert.equal(onError, true) + }) + + it('can approve the regular address and transfer', async function () { + let account1 = accounts[1] + await this.exhibition.approve(account1, this.editionID1) + let addr = await this.exhibition.getApproved(this.editionID1) + assert.equal(addr, account1) + await this.exhibition.safeTransferFrom(accounts[0], account1, this.editionID1, { + "from": account1 + }) + + let ownerOfEdition1 = await this.exhibition.ownerOf(this.editionID1); + assert.equal(ownerOfEdition1, account1); + }) + + it('can approve when unset the registry', async function () { + await this.exhibition.updateOperatorFilterRegistry(ZERO_ADDRESS) + let addressBefore = await this.exhibition.getApproved(this.editionID2) + assert.equal(addressBefore, ZERO_ADDRESS) + await this.exhibition.approve(this.operatorFilterRegistry.address, this.editionID2) + let addressAfter = await this.exhibition.getApproved(this.editionID2) + assert.equal(addressAfter, this.operatorFilterRegistry.address) + }) +}); diff --git a/test/feralfile_exhibition_v3_2_regression.js b/test/feralfile_exhibition_v3_2_regression.js new file mode 100644 index 0000000..b880086 --- /dev/null +++ b/test/feralfile_exhibition_v3_2_regression.js @@ -0,0 +1,401 @@ +const FeralfileExhibitionV3_2 = artifacts.require('FeralfileExhibitionV3_2'); + +const axios = require('axios'); + +const IPFS_GATEWAY_PREFIX = 'https://cloudflare-ipfs.com/ipfs/'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const originArtworkCID = 'QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc'; + +contract('FeralfileExhibitionV3_2', async (accounts) => { + before(async function () { + this.exhibition = await FeralfileExhibitionV3_2.new( + 'Feral File V3 Test 001', + 'FFV3', + 1000, + '0x8fd310de32848798eB64Bd88f9C5656Eea32415e', + 'https://ipfs.bitmark.com/ipfs/QmaptARVxNSP36PQai5oiCPqbrATvpydcJ8SPx6T6Yp1CZ', + IPFS_GATEWAY_PREFIX, + true, + true + ); + }); + + it('check contract is burnable', async function () { + let isBurnable = await this.exhibition.isBurnable(); + assert.equal(isBurnable, true); + }); + + it('check contract is bridgeable', async function () { + let isBridgeable = await this.exhibition.isBridgeable(); + assert.equal(isBridgeable, true); + }); + + it('create an artwork correctly with no editions on it', async function () { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', 'TestUser', 'TestArtwork', 10, 2, 1], + ]); + this.artworkID = r.logs[0].args.artworkID; + + let artwork = await this.exhibition.artworks(this.artworkID); + assert.equal(artwork.title, 'TestArtwork'); + + let artworkEditions = await this.exhibition.totalEditionOfArtwork( + this.artworkID + ); + assert.equal(artworkEditions, 0); + }); + + it('fail to create an artwork with duplicated fingerprint', async function () { + try { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', 'TestUser', 'TestArtwork', 5, 2, 1], + ]); + } catch (error) { + console.log(error.message); + + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert an artwork with the same fingerprint has already registered' + ); + } + }); + + it('fail to create an artwork with empty fingerprint', async function () { + try { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', 'TestUser', '', 10, 2, 1], + ]); + } catch (error) { + console.log(error.message); + + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert fingerprint can not be empty' + ); + } + }); + + it('fail to create an artwork with empty title', async function () { + try { + let r = await this.exhibition.createArtworks([ + ['', 'TestUser', 'TestArtwork', 5, 2, 1], + ]); + } catch (error) { + console.log(error.message); + + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert title can not be empty' + ); + } + }); + + it('fail to create an artwork with empty artist name', async function () { + try { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', '', 'TestArtwork', 5, 2, 1], + ]); + } catch (error) { + console.log(error.message); + + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert artist can not be empty' + ); + } + }); + + it('fail to create an artwork with empty edition size', async function () { + try { + let r = await this.exhibition.createArtworks([ + ['TestArtwork', 'TestArtwork', 'TestArtwork', 0, 2, 1], + ]); + } catch (error) { + console.log(error.message); + + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert edition size needs to be at least 1' + ); + } + }); + + it('can update the IPFS to a new one', async function () { + let newCID = 'QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXcNew'; + + let testEditionNumber = 1; + let editionID = BigInt(this.artworkID) + BigInt(testEditionNumber); + await this.exhibition.batchMint([ + [ + this.artworkID, + testEditionNumber, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test', + ], + ]); + await this.exhibition.updateArtworkEditionIPFSCid(editionID, newCID); + + let artworkEdition = await this.exhibition.artworkEditions(editionID); + + assert.equal(artworkEdition.ipfsCID, newCID); + }); + + it('cannot update the IPFS cid using an existent IPFS cid', async function () { + let testEditionNumber = 1; + let editionID = BigInt(this.artworkID) + BigInt(testEditionNumber); + try { + await this.exhibition.updateArtworkEditionIPFSCid( + editionID, + originArtworkCID + ); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert artwork edition is not found' + ); + } + }); + + it('cannot update the IPFS cid for a non-existent token', async function () { + let newCID = 'QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXcNew'; + + let editionID = 1; + try { + await this.exhibition.updateArtworkEditionIPFSCid(editionID, newCID); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert artwork edition is not found' + ); + } + }); + + it('can not set royalty payout address to zero address', async function () { + try { + await this.exhibition.setRoyaltyPayoutAddress(ZERO_ADDRESS); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert invalid royalty payout address' + ); + } + }); + + it('can not set royalty payout address to zero address', async function () { + try { + await this.exhibition.setRoyaltyPayoutAddress(ZERO_ADDRESS); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert invalid royalty payout address' + ); + } + }); + + it('test batch mint successfully in 1 transaction', async function () { + let editionNumber1 = 3; + let editionNumber2 = 4; + + // Mint to 0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2 and transfer to 0xD588e5EC7900C881Cec1843f2EbC73601D75e584 + await this.exhibition.batchMint([ + [this.artworkID, editionNumber1, accounts[0], accounts[0], 'test1'], + [ + this.artworkID, + editionNumber2, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test2', + ], + ]); + + let editionID1 = BigInt(this.artworkID) + BigInt(editionNumber1); + let ownerOfEdition1 = await this.exhibition.ownerOf(editionID1.toString()); + assert.equal(ownerOfEdition1.toLowerCase(), accounts[0].toLowerCase()); + + let editionID2 = BigInt(this.artworkID) + BigInt(editionNumber2); + let ownerOfEdition2 = await this.exhibition.ownerOf(editionID2.toString()); + assert.equal(ownerOfEdition2.toLowerCase(), accounts[1].toLowerCase()); + }); + + it('test batch transfer artworks successfully in 1 transaction', async function () { + let editionNumber = 5; + + // Mint to 0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2 and transfer to 0xD588e5EC7900C881Cec1843f2EbC73601D75e584 + await this.exhibition.batchMint([ + [ + this.artworkID, + editionNumber, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test', + ], + ]); + + let editionID = BigInt(this.artworkID) + BigInt(editionNumber); + + let expiryTime = (new Date().getTime() / 1000 + 300).toFixed(0); + + var instrucmentCode = web3.eth.abi.encodeParameters( + ['address', 'address', 'uint256', 'uint256'], + [ + accounts[1], + '0x487ba00d91015dcc905bb93b528c12a05fbc7a4f', + editionID.toString(), + expiryTime, + ] + ); + var hash = web3.utils.keccak256(instrucmentCode); + var sig = await web3.eth.sign(hash, accounts[1]); + + sig = sig.substr(2); + r = '0x' + sig.slice(0, 64); + s = '0x' + sig.slice(64, 128); + v = '0x' + sig.slice(128, 130); + + // Transfer item to 0x487ba00d91015dcc905bb93b528c12a05fbc7a4f + let transferredResult = await this.exhibition.authorizedTransfer([ + [ + accounts[1], + '0x487ba00d91015dcc905bb93b528c12a05fbc7a4f', + editionID.toString(), + expiryTime, + r, + s, + web3.utils.toDecimal(v) + 27, // magic 27 + ], + ]); + + let ownerOfEdition = await this.exhibition.ownerOf(editionID.toString()); + assert.equal( + ownerOfEdition.toLowerCase(), + '0x487ba00d91015dcc905bb93b528c12a05fbc7a4f'.toLowerCase() + ); + }); + + it('fail to batch transfer out of recv window', async function () { + let editionNumber = 6; + + // Mint to 0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2 and transfer to 0xD588e5EC7900C881Cec1843f2EbC73601D75e584 + await this.exhibition.batchMint([ + [ + this.artworkID, + editionNumber, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test6', + ], + ]); + + let editionID = BigInt(this.artworkID) + BigInt(editionNumber); + + let expiryTime = (new Date().getTime() / 1000 - 5).toFixed(0); + + try { + // Transfer item to 0x487ba00d91015dcc905bb93b528c12a05fbc7a4f + let transferredResult = await this.exhibition.authorizedTransfer([ + [ + accounts[1], + '0x487ba00d91015dcc905bb93b528c12a05fbc7a4f', + editionID.toString(), + expiryTime, + '0x4b1be9d4a91bf5f0462e96be0a67e738ac3ee2b191896c51b6b071f35c590563', + '0x5a3c40da5f67db32aa3e8026f55d3305bc40e025f9ccac5067ef47256bb49483', + '0x1b', + ], + ]); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert FeralfileExhibitionV3: the transfer request is expired' + ); + } + }); + + it('fail to batch transfer invalid signature', async function () { + let editionNumber = 7; + + // Mint to 0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2 and transfer to 0xD588e5EC7900C881Cec1843f2EbC73601D75e584 + await this.exhibition.batchMint([ + [ + this.artworkID, + editionNumber, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test7', + ], + ]); + + let editionID = BigInt(this.artworkID) + BigInt(editionNumber); + + let expiryTime = (new Date().getTime() / 1000 + 300).toFixed(0); + + try { + // Transfer item to 0x487ba00d91015dcc905bb93b528c12a05fbc7a4f + let transferredResult = await this.exhibition.authorizedTransfer([ + [ + accounts[1], + '0x487ba00d91015dcc905bb93b528c12a05fbc7a4f', + editionID.toString(), + expiryTime, + '0x4b1be9d4a91bf5f0462e96be0a67e738ac3ee2b191896c51b6b071f35c590563', + '0x5a3c40da5f67db32aa3e8026f55d3305bc40e025f9ccac5067ef47256bb49483', + '0x1b', + ], + ]); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert FeralfileExhibitionV3: the transfer request is not authorized' + ); + } + }); + + it('test burn edition successfully', async function () { + let editionNumber = 8; + + await this.exhibition.batchMint([ + [this.artworkID, editionNumber, accounts[0], accounts[0], 'test8'], + ]); + + let editionID = BigInt(this.artworkID) + BigInt(editionNumber); + + let editionCountBeforeBurn = await this.exhibition.totalEditionOfArtwork( + this.artworkID + ); + assert.equal(editionCountBeforeBurn.toNumber(), 7); + + await this.exhibition.burnEditions([editionID]); + let editionCountAfterBurn = await this.exhibition.totalEditionOfArtwork( + this.artworkID + ); + assert.equal(editionCountAfterBurn.toNumber(), 6); + }); + + it('test burn edition failed', async function () { + let editionNumber = 9; + + await this.exhibition.batchMint([ + [ + this.artworkID, + editionNumber, + '0xb824edfc5dced3ac86b6f5816763a35c2ba66fa2', + accounts[1], + 'test9', + ], + ]); + + let editionID = BigInt(this.artworkID) + BigInt(editionNumber); + + try { + let burnedResult = await this.exhibition.burnEditions([editionID]); + } catch (error) { + assert.equal( + error.message, + 'Returned error: VM Exception while processing transaction: revert ERC721: caller is not token owner nor approved' + ); + } + }); +});