UNPKG

@animoca/ethereum-contracts-sale

Version:
464 lines (379 loc) 16.9 kB
const {artifacts, web3} = require('hardhat'); const {BN, ether, expectEvent, expectRevert} = require('@openzeppelin/test-helpers'); const {stringToBytes32} = require('../utils/bytes32'); const {ZeroAddress, EmptyByte, Zero, One, Two, Three} = require('@animoca/ethereum-contracts-core').constants; const {makeNonFungibleTokenId} = require('@animoca/blockchain-inventory_metadata').inventoryIds; const {purchasingScenario} = require('../scenarios'); const Sale = artifacts.require('FixedOrderInventorySaleMock.sol'); const Inventory = artifacts.require('ERC1155721InventoryMock'); const ERC20 = artifacts.require('ERC20Mock'); const erc20TotalSupply = ether('1000000000'); const purchaserErc20Balance = ether('100000'); const recipientErc20Balance = ether('100000'); const erc20Price = ether('1'); const nfMaskLength = 32; const token1 = makeNonFungibleTokenId(1, 1, nfMaskLength); const token2 = makeNonFungibleTokenId(2, 1, nfMaskLength); const token3 = makeNonFungibleTokenId(3, 1, nfMaskLength); const tokens = [token1, token2, token3]; const tokensPerSkuCapacity = Two; const sku = stringToBytes32('sku'); const maxQuantityPerPurchase = Three; const notificationsReceiver = ZeroAddress; const userData = EmptyByte; let deployer, payoutWallet, purchaser, recipient, other; describe('FixedOrderInventorySale', function () { before(async function () { [deployer, payoutWallet, purchaser, recipient, other] = await web3.eth.getAccounts(); }); async function doDeployErc20(overrides = {}) { const registry = await artifacts.require('ForwarderRegistry').new(); const forwarder = await artifacts.require('UniversalForwarder').new(); this.erc20 = await ERC20.new( [overrides.from || deployer], [overrides.erc20TotalSupply || erc20TotalSupply], registry.address, forwarder.address, { from: overrides.from || deployer, } ); await this.erc20.transfer(overrides.purchaser || purchaser, overrides.purchaserErc20Balance || purchaserErc20Balance, { from: overrides.from || deployer, }); await this.erc20.transfer(overrides.recipient || recipient, overrides.recipientErc20Balance || recipientErc20Balance, { from: overrides.from || deployer, }); } async function doDeployInventory(overrides = {}) { this.inventory = await Inventory.new({from: overrides.from || deployer}); this.tokens = overrides.tokens || tokens; } async function doDeploySale(overrides = {}) { this.sale = await Sale.new( overrides.inventory || this.inventory.address, overrides.payoutWallet || payoutWallet, overrides.tokensPerSkuCapacity || tokensPerSkuCapacity, {from: overrides.from || deployer} ); } async function doAddSupply(overrides = {}) { await this.sale.addSupply(overrides.tokens || tokens, {from: overrides.from || deployer}); } async function doCreateSku(overrides = {}) { await this.sale.methods['createSku(bytes32,uint256,address)']( overrides.sku || sku, overrides.maxQuantityPerPurchase || maxQuantityPerPurchase, overrides.notificationReceiver || notificationsReceiver, {from: overrides.from || deployer} ); } async function doUpdateSkuPricing(overrides = {}) { await this.sale.updateSkuPricing(overrides.sku || sku, [overrides.erc20Address || this.erc20.address], [overrides.erc20Price || erc20Price], { from: overrides.from || deployer, }); } describe('constructor', function () { beforeEach(async function () { await doDeployInventory.bind(this)(); }); describe('when `inventory_` is the zero address', function () { it('reverts', async function () { await expectRevert(doDeploySale.bind(this)({inventory: ZeroAddress}), 'Sale: zero address inventory'); }); }); describe('when successful', function () { beforeEach(async function () { await doDeploySale.bind(this)(); }); it('supports a single sale SKU', async function () { const skuCapacity = await this.sale.getSkuCapacity(); skuCapacity.should.be.bignumber.equal(One); }); it('sets the inventory contract correctly', async function () { const inventory = await this.sale.inventory(); inventory.should.be.equal(this.inventory.address); }); it('sets the tokens per-sku capacity correctly', async function () { const capacity = await this.sale.getTokensPerSkuCapacity(); capacity.should.be.bignumber.equal(tokensPerSkuCapacity); }); }); }); describe('addSupply()', function () { const token4 = makeNonFungibleTokenId(4, 1, nfMaskLength); const token5 = makeNonFungibleTokenId(5, 1, nfMaskLength); const tokens = [token4, token5]; beforeEach(async function () { await doDeployInventory.bind(this)(); await doDeploySale.bind(this)(); }); describe('when called by any other than the contract owner', function () { it('reverts', async function () { await expectRevert(this.sale.addSupply(tokens, {from: other}), 'Ownable: not the owner'); }); }); describe('when `tokens` is empty', function () { it('reverts', async function () { await expectRevert(this.sale.addSupply([], {from: deployer}), 'Sale: empty tokens'); }); }); describe('when any of `tokens` are zero', function () { it('reverts', async function () { await expectRevert(this.sale.addSupply([token4, Zero, token5], {from: deployer}), 'Sale: zero supply'); }); }); describe('when successful', function () { const addSupply = () => { beforeEach(async function () { this.tokenList = await this.sale.getTokenList(); await this.sale.addSupply(tokens, {from: deployer}); }); it('appends the tokens to the sale supply correctly', async function () { const tokenList = await this.sale.getTokenList(); this.tokenList.push(...tokens.map((token) => new BN(token))); this.tokenList.length.should.equal(tokenList.length); for (var index = 0; index < this.tokenList.length; ++index) { tokenList[index].toString().should.equal(this.tokenList[index].toString()); } }); }; context('when the SKU for the sale inventory has not been created yet', function () { addSupply(); }); context('when the SKU for the sale inventory has been created', function () { beforeEach(async function () { await doAddSupply.bind(this)(); await doCreateSku.bind(this)(); this.skuInfo = await this.sale.getSkuInfo(sku); }); addSupply(); it('updates the sku total supply correctly', async function () { const skuInfo = await this.sale.getSkuInfo(sku); skuInfo.totalSupply.should.be.bignumber.equal(this.skuInfo.totalSupply.addn(tokens.length)); }); it('updates the sku remaining supply correctly', async function () { const skuInfo = await this.sale.getSkuInfo(sku); skuInfo.remainingSupply.should.be.bignumber.equal(this.skuInfo.remainingSupply.addn(tokens.length)); }); }); }); }); describe('setSupply()', function () { beforeEach(async function () { await doDeployInventory.bind(this)(); await doDeploySale.bind(this)(); }); context('when called by any other than the contract owner', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([0], [token2], {from: other}), 'Ownable: not the owner'); }); }); context('when the contract is not paused', function () { it('reverts', async function () { await this.sale.start({from: deployer}); await expectRevert(this.sale.setSupply([0], [token2], {from: deployer}), 'Pausable: not paused'); }); }); context('when the token list is empty', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([0], [token2], {from: deployer}), 'Sale: empty tokens'); }); }); context('when the token list is initialized', function () { beforeEach(async function () { await doAddSupply.bind(this)(); }); describe('when the lengths of `indexes` and `tokens` do not match', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([0], [token2, token3], {from: deployer}), 'Sale: inconsistent arrays'); }); }); describe('when `indexes` is zero length', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([], [token2], {from: deployer}), 'Sale: empty indexes'); }); }); describe('when any of the indexes are less than the token index', function () { beforeEach(async function () { await doDeployErc20.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); await this.sale.start({from: deployer}); const quantity = One; await this.erc20.approve(this.sale.address, quantity.mul(erc20Price), {from: purchaser}); await this.sale.purchaseFor(recipient, this.erc20.address, sku, quantity, userData, {from: purchaser}); await this.sale.pause({from: deployer}); }); it('reverts', async function () { await expectRevert(this.sale.setSupply([0], [token2], {from: deployer}), 'Sale: invalid index'); }); }); describe('when any of `indexes` are out-of-bounds', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([tokens.length], [token2], {from: deployer}), 'Sale: index out-of-bounds'); }); }); describe('when `tokens` is zero length', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([0], [], {from: deployer}), 'Sale: inconsistent arrays'); }); }); describe('when any of `tokens` are zero', function () { it('reverts', async function () { await expectRevert(this.sale.setSupply([0, 1, 2], [token2, Zero, token1], {from: deployer}), 'Sale: zero supply'); }); }); describe('when successful', function () { beforeEach(async function () { this.tokenList = await this.sale.getTokenList(); this.tokens = new Array(this.tokenList.length); const indexes = new Array(this.tokenList.length); for (var index = 0; index < this.tokenList.length; index++) { var i = (index + 1) % this.tokenList.length; this.tokens[i] = this.tokenList[index]; indexes[index] = index; } await this.sale.setSupply(indexes, this.tokens, {from: deployer}); }); it('should set the supply correctly', async function () { const tokenList = await this.sale.getTokenList(); tokenList.length.should.equal(this.tokens.length); for (var index = 0; index < tokenList.length; index++) { tokenList[index].should.be.bignumber.equal(this.tokens[index]); } }); }); }); }); describe('getTotalSupply()', function () { const token4 = makeNonFungibleTokenId(4, 1, nfMaskLength); const token5 = makeNonFungibleTokenId(5, 1, nfMaskLength); const addedTokens = [token4, token5]; beforeEach(async function () { await doDeployInventory.bind(this)(); await doDeploySale.bind(this)(); }); context('when there is no initial sale supply', function () { context('when the sku has not been created', function () { context('when additional supply is not added', function () { it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(Zero); }); }); context('when additional supply is added', function () { beforeEach(async function () { await doAddSupply.bind(this)({tokens: addedTokens}); }); it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(new BN(addedTokens.length)); }); }); }); }); context('when there is an initial sale supply', function () { beforeEach(async function () { await doAddSupply.bind(this)(); }); context('when the sku has not been created', function () { context('when additional supply is not added', function () { it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(new BN(tokens.length)); }); }); context('when additional supply is added', function () { beforeEach(async function () { await doAddSupply.bind(this)({tokens: addedTokens}); }); it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(new BN(tokens.length + addedTokens.length)); }); }); }); context('when the sku has been created', function () { beforeEach(async function () { await doCreateSku.bind(this)(); }); context('when additional supply is not added', function () { it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(new BN(tokens.length)); const skuInfo = await this.sale.getSkuInfo(sku); totalSupply.should.be.bignumber.equal(skuInfo.totalSupply); }); }); context('when additional supply is added', function () { beforeEach(async function () { await doAddSupply.bind(this)({tokens: addedTokens}); }); it('retrieves the correct total supply', async function () { const totalSupply = await this.sale.getTotalSupply(); totalSupply.should.be.bignumber.equal(new BN(tokens.length + addedTokens.length)); const skuInfo = await this.sale.getSkuInfo(sku); totalSupply.should.be.bignumber.equal(skuInfo.totalSupply); }); }); }); }); }); describe('_delivery()', function () { beforeEach(async function () { await doDeployErc20.bind(this)(); await doDeployInventory.bind(this)(); await doDeploySale.bind(this)(); await doAddSupply.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); }); describe('when successful', function () { const quantity = Two; beforeEach(async function () { this.tokenList = await this.sale.getTokenList(); this.tokenIndex = await this.sale.tokenIndex(); this.receipt = await this.sale.underscoreDelivery(recipient, this.erc20.address, sku, quantity, userData, quantity.mul(erc20Price), [], [], { from: purchaser, }); }); it('updates the token index correctly', async function () { const tokenIndex = await this.sale.tokenIndex(); tokenIndex.should.be.bignumber.equal(this.tokenIndex.add(quantity)); }); it('updates the purchase delivery data correctly', async function () { var tokens = []; var index = this.tokenIndex.toNumber(); for (var count = 0; count < quantity.toNumber(); count++) { tokens.push('0x' + this.tokenList[index++].toString(16)); } expectEvent(this.receipt, 'DeliveryData', { tokens: tokens, }); }); }); }); describe('scenarios', function () { beforeEach(async function () { await doDeployErc20.bind(this)(); await doDeployInventory.bind(this)(); await doDeploySale.bind(this)(); await doAddSupply.bind(this)(); await doCreateSku.bind(this)(); await doUpdateSkuPricing.bind(this)(); await this.sale.start({from: deployer}); }); describe('purchasing', function () { beforeEach(async function () { await this.erc20.transfer(purchaser, ether('1')); await this.erc20.transfer(recipient, ether('1')); this.contract = this.sale; this.erc20TokenAddress = this.erc20.address; this.purchaser = purchaser; this.recipient = recipient; }); purchasingScenario(sku, userData, {onlyErc20: true}); }); }); });