UNPKG

@animoca/ethereum-contracts-assets

Version:
400 lines (364 loc) 17.8 kB
const {accounts, web3} = require('hardhat'); const {createFixtureLoader} = require('@animoca/ethereum-contracts-core/test/utils/fixture'); const {expectEventWithParamsOverride} = require('@animoca/ethereum-contracts-core/test/utils/events'); const {BN, expectEvent, expectRevert} = require('@openzeppelin/test-helpers'); const {makeFungibleCollectionId, makeNonFungibleCollectionId, makeNonFungibleTokenId, isNonFungibleToken, isFungible} = require('@animoca/blockchain-inventory_metadata').inventoryIds; const {Zero, One, ZeroAddress} = require('@animoca/ethereum-contracts-core').constants; const interfaces1155 = require('../../../../../src/interfaces/ERC165/ERC1155'); const interfaces1155721 = require('../../../../../src/interfaces/ERC165/ERC1155721'); const {behaviors} = require('@animoca/ethereum-contracts-core'); function shouldBehaveLikeERC1155Burnable({nfMaskLength, contractName, revertMessages, eventParamsOverrides, interfaces, methods, deploy, mint}) { const [deployer, owner, operator, approved, other] = accounts; const {'burnFrom(address,uint256,uint256)': burnFrom_ERC1155, 'batchBurnFrom(address,uint256[],uint256[])': batchBurnFrom_ERC1155} = methods; if (burnFrom_ERC1155 === undefined) { console.log( `ERC1155InventoryBurnable: non-standard ERC1155 method burnFrom(address,uint256,uint256)` + `is not supported by ${contractName}, associated tests will be skipped` ); } if (batchBurnFrom_ERC1155 === undefined) { console.log( `ERC1155InventoryBurnable: non-standard ERC1155 method batchBurnFrom(address,uint256[],uint256[])` + `is not supported by ${contractName}, associated tests will be skipped` ); } describe('like a burnable ERC1155Inventory', function () { const fCollection1 = { id: makeFungibleCollectionId(1), supply: 10, }; const fCollection2 = { id: makeFungibleCollectionId(2), supply: 11, }; const fCollection3 = { id: makeFungibleCollectionId(3), supply: 12, }; const unknownFCollection = { id: makeFungibleCollectionId(4), supply: 0, }; const nfCollection = makeNonFungibleCollectionId(1, nfMaskLength); const nfCollectionOther = makeNonFungibleCollectionId(2, nfMaskLength); const unknownNFCollection = makeNonFungibleCollectionId(99, nfMaskLength); const nft1 = makeNonFungibleTokenId(1, 1, nfMaskLength); const nft2 = makeNonFungibleTokenId(2, 1, nfMaskLength); const nftOtherCollection = makeNonFungibleTokenId(1, 2, nfMaskLength); const unknownNft = makeNonFungibleTokenId(99, 99, nfMaskLength); const fixtureLoader = createFixtureLoader(accounts, web3.eth.currentProvider); const fixture = async function () { this.token = await deploy(deployer); await mint(this.token, owner, fCollection1.id, fCollection1.supply, {from: deployer}); await mint(this.token, owner, fCollection2.id, fCollection2.supply, {from: deployer}); await mint(this.token, owner, fCollection3.id, fCollection3.supply, {from: deployer}); await mint(this.token, owner, nft1, 1, {from: deployer}); await mint(this.token, owner, nft2, 1, {from: deployer}); await mint(this.token, owner, nftOtherCollection, 1, {from: deployer}); await this.token.setApprovalForAll(operator, true, {from: owner}); if (interfaces.ERC721) { await this.token.approve(approved, nft1, {from: owner}); await this.token.approve(approved, nft2, {from: owner}); // await this.token.approve(approved, nftOtherCollection, {from: owner}); } // pre-transfer state if (interfaces.ERC721) { this.nftBalance = await this.token.balanceOf(owner); } if (interfaces.ERC1155Inventory) { this.nfcBalance = await this.token.balanceOf(owner, nfCollection); this.otherNFCBalance = await this.token.balanceOf(owner, nfCollectionOther); if (interfaces.ERC1155InventoryTotalSupply) { this.nfcSupply = await this.token.totalSupply(nfCollection); this.otherNFCSupply = await this.token.totalSupply(nfCollectionOther); } } }; beforeEach(async function () { await fixtureLoader(fixture, this); }); let receipt = null; const burnWasSuccessful = function (tokenIds, values, options) { const ids = Array.isArray(tokenIds) ? tokenIds : [tokenIds]; const vals = Array.isArray(values) ? values : [values]; const tokens = ids.map((id, i) => [id, vals[i]]); const fungibleTokens = tokens.filter(([id, _value]) => isFungible(id)); const nonFungibleTokens = tokens.filter(([id, _value]) => isNonFungibleToken(id, nfMaskLength)); if (tokens.length != 0) { it('decreases the sender balance(s)', async function () { for (const [id, value] of tokens) { let balance; if (isNonFungibleToken(id, nfMaskLength)) { balance = 0; (await this.token.balanceOf(owner, id)).should.be.bignumber.equal(Zero); } else { if (id == fCollection1.id) { balance = fCollection1.supply; } else if (id == fCollection2.id) { balance = fCollection2.supply; } else if (id == fCollection3.id) { balance = fCollection3.supply; } balance = balance - value; } (await this.token.balanceOf(owner, id)).should.be.bignumber.equal(new BN(balance)); } }); if (interfaces.ERC1155InventoryTotalSupply) { it('[ERC1155InventoryTotalSupply] decreases the token(s) total supply', async function () { for (const [id, value] of tokens) { let supply; if (isNonFungibleToken(id, nfMaskLength)) { supply = 0; } else { if (id == fCollection1.id) { supply = fCollection1.supply; } else if (id == fCollection2.id) { supply = fCollection2.supply; } else if (id == fCollection3.id) { supply = fCollection3.supply; } supply = supply - value; } (await this.token.totalSupply(id)).should.be.bignumber.equal(new BN(supply)); } }); } if (nonFungibleTokens.length != 0) { if (interfaces.ERC721 || interfaces.ERC1155Inventory) { it('[ERC721/ERC1155Inventory] removes the ownership of the Non-Fungible Token(s)', async function () { for (const [id, _value] of nonFungibleTokens) { await expectRevert(this.token.ownerOf(id), revertMessages.NonExistingNFT); } }); } if (interfaces.ERC1155Inventory) { const nbCollectionNFTs = nonFungibleTokens.filter(([id]) => id == nft1 || id == nft2).length; const nbOtherCollectionNFTs = nonFungibleTokens.filter(([id]) => id == nftOtherCollection).length; it('[ERC1155Inventory] decreases the sender Non-Fungible Collection(s) balance(s)', async function () { (await this.token.balanceOf(owner, nfCollection)).should.be.bignumber.equal(this.nfcBalance.subn(nbCollectionNFTs)); (await this.token.balanceOf(owner, nfCollectionOther)).should.be.bignumber.equal(this.otherNFCBalance.subn(nbOtherCollectionNFTs)); }); if (interfaces.ERC1155InventoryTotalSupply) { it('[ERC1155InventoryTotalSupply] decreases the Non-Fungible Collection(s) total supply', async function () { (await this.token.totalSupply(nfCollection)).should.be.bignumber.equal(this.nfcSupply.subn(nbCollectionNFTs)); (await this.token.totalSupply(nfCollectionOther)).should.be.bignumber.equal(this.otherNFCSupply.subn(nbOtherCollectionNFTs)); }); it('[ERC1155InventoryTotalSupply] sets the Non-Fungible Token(s) total supply to 0', async function () { for (const [id, _value] of nonFungibleTokens) { (await this.token.totalSupply(id)).should.be.bignumber.equal('0'); } }); } } if (interfaces.ERC721) { it('[ERC721] decreases the sender NFTs balance', async function () { (await this.token.balanceOf(owner)).should.be.bignumber.equal(this.nftBalance.subn(nonFungibleTokens.length)); }); it('[ERC721] emits Transfer event(s) for Non-Fungible Token(s)', function () { for (const [id, value] of nonFungibleTokens) { expectEventWithParamsOverride( receipt, 'Transfer', { _from: owner, _to: ZeroAddress, _tokenId: id, }, eventParamsOverrides ); } }); } } } if (Array.isArray(tokenIds)) { it('emits a TransferBatch event', function () { expectEventWithParamsOverride( receipt, 'TransferBatch', { _operator: options.from, _from: owner, _to: ZeroAddress, _ids: tokenIds, _values: values, }, eventParamsOverrides ); }); } else { it('emits a TransferSingle event', function () { expectEventWithParamsOverride( receipt, 'TransferSingle', { _operator: options.from, _from: owner, _to: ZeroAddress, _id: tokenIds, _value: values, }, eventParamsOverrides ); }); } }; const shouldBurnTokenBySender = function (burnFunction, tokenIds, values) { context('when called by the owner', function () { const options = {from: owner}; beforeEach(async function () { receipt = await burnFunction.call(this, owner, tokenIds, values, options); }); burnWasSuccessful(tokenIds, values, options); }); context('when called by an operator', function () { const options = {from: operator}; beforeEach(async function () { receipt = await burnFunction.call(this, owner, tokenIds, values, options); }); burnWasSuccessful(tokenIds, values, options); }); if (interfaces.ERC721) { const ids = Array.isArray(tokenIds) ? tokenIds : [tokenIds]; const approvedTokenIds = ids.filter((id) => id == nft1 || id == nft2); // All tokens are approved NFTs if (ids.length != 0 && ids.length == approvedTokenIds.length) { context('[ERC721] when called by a wallet with single token approval', function () { const options = {from: approved}; beforeEach(async function () { receipt = await burnFunction.call(this, owner, tokenIds, values, options); }); burnWasSuccessful(tokenIds, values, options); }); } } }; const shouldRevertOnPreconditions = function (burnFunction) { describe('Pre-conditions', function () { if (interfaces.Pausable) { it('[Pausable] reverts when paused', async function () { await this.token.pause({from: deployer}); await expectRevert(burnFunction.call(this, owner, nft1, 1, {from: owner}), revertMessages.AlreadyPaused); }); } it('reverts if the sender is not approved', async function () { await expectRevert(burnFunction.call(this, owner, nft1, 1, {from: other}), revertMessages.NonApproved); await expectRevert(burnFunction.call(this, owner, fCollection1.id, 1, {from: other}), revertMessages.NonApproved); }); it('reverts if a Fungible Token has a value equal 0', async function () { await expectRevert(burnFunction.call(this, owner, fCollection1.id, 0, {from: owner}), revertMessages.ZeroValue); }); it('reverts if a Non-Fungible Token has a value different from 1', async function () { await expectRevert(burnFunction.call(this, owner, nft1, 0, {from: owner}), revertMessages.WrongNFTValue); await expectRevert(burnFunction.call(this, owner, nft1, 2, {from: owner}), revertMessages.WrongNFTValue); }); it('reverts with a non-existing Non-Fungible Token', async function () { await expectRevert(burnFunction.call(this, owner, unknownNft, 1, {from: owner}), revertMessages.NonOwnedNFT); }); it('reverts if from is not the owner for a Non-Fungible Token', async function () { await expectRevert(burnFunction.call(this, other, nft1, 1, {from: other}), revertMessages.NonOwnedNFT); }); it('reverts if from has insufficient balance for a Fungible Token', async function () { await expectRevert(burnFunction.call(this, other, fCollection1.id, 1, {from: other}), revertMessages.InsufficientBalance); }); if (interfaces.ERC721) { it('[ERC721] reverts if the sender is not authorized for the token', async function () { await expectRevert(burnFunction.call(this, owner, nft1, 1, {from: other}), revertMessages.NonApproved); }); } if (interfaces.ERC1155Inventory) { it('[ERC1155Inventory] reverts if the id is a Non-Fungible Collection', async function () { await expectRevert(burnFunction.call(this, owner, nfCollection, 1, {from: owner}), revertMessages.NotToken); }); } }); }; const shouldBurnToken = function (burnFunction, ids, values) { shouldBurnTokenBySender(burnFunction, ids, values); }; describe('burnFrom(address,uint256,uint256)', function () { if (burnFrom_ERC1155 === undefined) { return; } const burnFn = async function (from, id, value, options) { return burnFrom_ERC1155(this.token, from, id, value, options); }; shouldRevertOnPreconditions(burnFn); context('with a Fungible Token', function () { context('partial balance transfer', function () { shouldBurnToken(burnFn, fCollection1.id, 1, '0x42'); }); context('full balance transfer', function () { shouldBurnToken(burnFn, fCollection1.id, fCollection1.supply, '0x42'); }); }); context('with a Non-Fungible Token', function () { shouldBurnToken(burnFn, nft1, 1, '0x42'); }); }); describe('batchBurnFrom(address,uint256[],uint256[])', function () { if (batchBurnFrom_ERC1155 === undefined) { return; } const burnFn = async function (from, ids, values, options) { const tokenIds = Array.isArray(ids) ? ids : [ids]; const vals = Array.isArray(values) ? values : [values]; return batchBurnFrom_ERC1155(this.token, from, tokenIds, vals, options); }; shouldRevertOnPreconditions(burnFn); it('reverts with inconsistent arrays', async function () { await expectRevert(burnFn.call(this, owner, [nft1, nft2], [1], {from: owner}), revertMessages.InconsistentArrays); }); context('with an empty list of tokens', function () { shouldBurnToken(burnFn, [], []); }); context('with Fungible Tokens', function () { context('single partial balance transfer', function () { shouldBurnToken(burnFn, [fCollection1.id], [1]); }); context('single full balance transfer', function () { shouldBurnToken(burnFn, [fCollection1.id], [fCollection1.supply]); }); context('multiple tokens transfer', function () { shouldBurnToken(burnFn, [fCollection1.id, fCollection2.id, fCollection3.id], [fCollection1.supply, 1, fCollection3.supply]); }); }); context('with Non-Fungible Tokens', function () { context('single token transfer', function () { shouldBurnToken(burnFn, [nft1], [1]); }); context('multiple tokens from the same collection transfer', function () { shouldBurnToken(burnFn, [nft1, nft2], [1, 1]); }); context('multiple tokens sorted by collection transfer', function () { shouldBurnToken(burnFn, [nft1, nft2, nftOtherCollection], [1, 1, 1]); }); if (interfaces.ERC1155Inventory) { context('[ERC1155Inventory] multiple tokens not sorted by collection transfer', function () { shouldBurnToken(burnFn, [nft1, nftOtherCollection, nft2], [1, 1, 1]); }); } }); context('with Fungible and Non-Fungible Tokens', function () { context('multiple tokens sorted by Non-Fungible Collection transfer', function () { shouldBurnToken(burnFn, [fCollection1.id, nft1, fCollection2.id, nft2, nftOtherCollection], [2, 1, fCollection2.supply, 1, 1]); }); if (interfaces.ERC1155Inventory) { context('multiple tokens not sorted by Non-Fungible Collection transfer', function () { shouldBurnToken(burnFn, [fCollection1.id, nft1, fCollection2.id, nftOtherCollection, nft2], [2, 1, fCollection2.supply, 1, 1]); }); } }); }); if (interfaces.ERC1155InventoryBurnable) { behaviors.shouldSupportInterfaces([interfaces1155.ERC1155InventoryBurnable]); } else if (interfaces.ERC1155721InventoryBurnable) { behaviors.shouldSupportInterfaces([interfaces1155721.ERC1155721InventoryBurnable]); } }); } module.exports = { shouldBehaveLikeERC1155Burnable, };