@animoca/ethereum-contracts-sale
Version:
782 lines (676 loc) • 31.3 kB
JavaScript
const {artifacts} = require('hardhat');
const {ether, expectEvent, expectRevert} = require('@openzeppelin/test-helpers');
const {ZeroAddress, Zero, One, Two, Three} = require('@animoca/ethereum-contracts-core').constants;
const {addressToBytes32, stringToBytes32, uintToBytes32, bytes32ArraysToBytesPacked} = require('../../utils/bytes32');
const Sale = artifacts.require('SaleMock');
const ERC20 = artifacts.require('ERC20Mock');
const PurchaseNotificationsReceiver = artifacts.require('PurchaseNotificationsReceiverMock');
const skusCapacity = Two;
const tokensPerSkuCapacity = Two;
const sku = stringToBytes32('sku');
const skuTotalSupply = Three;
const skuMaxQuantityPerPurchase = Two;
const skuNotificationsReceiver = ZeroAddress;
const erc20TotalSupply = ether('1000000000');
const purchaserErc20Balance = ether('100000');
const recipientErc20Balance = ether('100000');
const erc20Price = ether('1');
const ethPrice = ether('0.01');
const userData = '0x00';
let _, owner, payoutWallet, purchaser, recipient;
describe('Sale', function () {
before(async function () {
[_, owner, payoutWallet, purchaser, recipient] = await web3.eth.getAccounts();
});
async function doDeploy(params = {}) {
this.contract = await Sale.new(
params.payoutWallet || payoutWallet,
params.skusCapacity || skusCapacity,
params.tokensPerSkuCapacity || tokensPerSkuCapacity,
{from: params.owner || owner}
);
}
async function doCreateSku(params = {}) {
return await this.contract.createSku(
params.sku || sku,
params.skuTotalSupply || skuTotalSupply,
params.skuMaxQuantityPerPurchase || skuMaxQuantityPerPurchase,
params.skuNotificationsReceiver || skuNotificationsReceiver,
{from: params.owner || owner}
);
}
async function doUpdateSkuPricing(params = {}) {
this.ethTokenAddress = await this.contract.TOKEN_ETH();
if (params.useErc20) {
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
this.erc20Token = await ERC20.new([params.owner || owner], [params.erc20TotalSupply || erc20TotalSupply], registry.address, forwarder.address, {
from: params.owner || owner,
});
await this.erc20Token.transfer(params.purchaser || purchaser, params.purchaserErc20Balance || purchaserErc20Balance, {
from: params.owner || owner,
});
await this.erc20Token.transfer(params.recipient || recipient, params.recipientErc20Balance || recipientErc20Balance, {
from: params.owner || owner,
});
this.tokenAddress = this.erc20Token.address;
this.tokenPrice = params.erc20Price || erc20Price;
} else {
this.tokenAddress = this.ethTokenAddress;
this.tokenPrice = params.ethPrice || ethPrice;
}
return await this.contract.updateSkuPricing(params.sku || sku, [this.tokenAddress], [this.tokenPrice], {from: params.owner || owner});
}
async function doStart(params = {}) {
return await this.contract.start({from: params.owner || owner});
}
describe('start()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
});
it('reverts if called by any other than the contract owner', async function () {
await expectRevert(doStart.bind(this)({owner: purchaser}), 'Ownable: not the owner');
});
it('reverts if the contract has already started', async function () {
await doStart.bind(this)();
await expectRevert(doStart.bind(this)(), 'Startable: started');
});
it('reverts if the contract is not paused', async function () {
await this.contract.setPaused(false);
await expectRevert(doStart.bind(this)(), 'Pausable: not paused');
});
it('should start the contract', async function () {
const startedAtBefore = await this.contract.startedAt();
startedAtBefore.should.be.bignumber.equal(Zero);
await doStart.bind(this)();
const startedAtAfter = await this.contract.startedAt();
startedAtAfter.should.be.bignumber.gt(Zero);
});
it('should unpause the contract', async function () {
const pausedBefore = await this.contract.paused();
pausedBefore.should.be.true;
await doStart.bind(this)();
const pausedAfter = await this.contract.paused();
pausedAfter.should.be.false;
});
});
describe('pause()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
});
it('reverts if the contract has not been started yet', async function () {
await expectRevert(this.contract.pause({from: owner}), 'Startable: not started');
});
it('reverts if called by any other than the contract owner', async function () {
await doStart.bind(this)();
await expectRevert(this.contract.pause({from: purchaser}), 'Ownable: not the owner');
});
it('reverts if the contract is already paused', async function () {
await doStart.bind(this)();
await this.contract.pause({from: owner});
await expectRevert(this.contract.pause({from: owner}), 'Pausable: paused');
});
it('should pause the contract', async function () {
await doStart.bind(this)();
const pausedBefore = await this.contract.paused();
pausedBefore.should.be.false;
await this.contract.pause({from: owner});
const pausedAfter = await this.contract.paused();
pausedAfter.should.be.true;
});
});
describe('unpause()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
});
it('reverts if called by any other than the contract owner', async function () {
await doStart.bind(this)();
await this.contract.pause({from: owner});
await expectRevert(this.contract.unpause({from: purchaser}), 'Ownable: not the owner');
});
it('reverts if the contract has not been started yet', async function () {
await expectRevert(this.contract.unpause({from: owner}), 'Startable: not started');
});
it('reverts if the contract is not paused', async function () {
await doStart.bind(this)();
await expectRevert(this.contract.unpause({from: owner}), 'Pausable: not paused');
});
it('should resume the contract', async function () {
await doStart.bind(this)();
await this.contract.pause({from: owner});
const pausedBefore = await this.contract.paused();
pausedBefore.should.be.true;
await this.contract.unpause({from: owner});
const pausedAfter = await this.contract.paused();
pausedAfter.should.be.false;
});
});
describe('updateSkuPricing()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
});
it('reverts if called by any other than the contract owner', async function () {
await expectRevert(doUpdateSkuPricing.bind(this)({owner: purchaser}), 'Ownable: not the owner');
});
it('reverts if `tokens` and `prices` have different lengths', async function () {
const ethTokenAddress = await this.contract.TOKEN_ETH();
await expectRevert(this.contract.updateSkuPricing(sku, [ethTokenAddress], [], {from: owner}), 'Sale: inconsistent arrays');
});
it('reverts if `sku` does not exist', async function () {
await expectRevert(doUpdateSkuPricing.bind(this)({sku: stringToBytes32('otherSku')}), 'Sale: non-existent sku');
});
it('should disable a SKU if provided empty `tokens`/`prices` lists', async function () {
await doUpdateSkuPricing.bind(this)();
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.tokens.length.should.equal(1);
await this.contract.updateSkuPricing(sku, [], [], {from: owner});
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.tokens.length.should.equal(0);
});
it("should update a SKU's set of supported token prices", async function () {
await doUpdateSkuPricing.bind(this)();
await doUpdateSkuPricing.bind(this)({useErc20: true});
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.tokens.length.should.equal(2);
skuInfoBefore.tokens[0].should.equal(this.ethTokenAddress);
skuInfoBefore.prices[0].should.be.bignumber.equal(ethPrice);
skuInfoBefore.tokens[1].should.be.equal(this.erc20Token.address);
skuInfoBefore.prices[1].should.be.bignumber.equal(erc20Price);
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
const erc20Token = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
const updatedErc20Price = erc20Price.muln(2);
const newErc20Price = erc20Price.muln(3);
await this.contract.updateSkuPricing(
sku,
[this.ethTokenAddress, this.erc20Token.address, erc20Token.address],
[Zero, updatedErc20Price, newErc20Price],
{from: owner}
);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.tokens.length.should.equal(2);
skuInfoAfter.tokens[0].should.be.equal(this.erc20Token.address);
skuInfoAfter.prices[0].should.be.bignumber.equal(updatedErc20Price);
skuInfoAfter.tokens[1].should.be.equal(erc20Token.address);
skuInfoAfter.prices[1].should.be.bignumber.equal(newErc20Price);
});
it('should emit the SkuPricingUpdate event', async function () {
const receipt = await doUpdateSkuPricing.bind(this)();
expectEvent(receipt, 'SkuPricingUpdate', {
sku: sku,
tokens: [this.ethTokenAddress],
prices: [ethPrice],
});
});
});
describe('purchaseFor()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
await doUpdateSkuPricing.bind(this)();
});
it('reverts if the sale has not started', async function () {
const quantity = One;
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Startable: not started'
);
});
it('reverts if the sale is paused', async function () {
await doStart.bind(this)();
await this.contract.pause({from: owner});
const quantity = One;
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Pausable: paused'
);
});
it('reverts if `recipient` is the zero address', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.purchaseFor(ZeroAddress, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: zero address recipient'
);
});
it('reverts if `token` is the zero address', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.purchaseFor(recipient, ZeroAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: zero address token'
);
});
it('reverts if `quantity` is zero', async function () {
await doStart.bind(this)();
const quantity = Zero;
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: zero quantity purchase'
);
});
it('reverts if `sku` does not exist', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, stringToBytes32('otherSku'), quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: non-existent sku'
);
});
it('reverts if `quantity` is greater than the maximum purchase quantity', async function () {
await doStart.bind(this)();
const quantity = skuMaxQuantityPerPurchase.addn(1);
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: above max quantity'
);
});
it('reverts if `quantity` is greater than the remaining supply', async function () {
await doStart.bind(this)();
const quantity = skuMaxQuantityPerPurchase;
await this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
});
await expectRevert(
this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
}),
'Sale: insufficient supply'
);
});
it('reverts if `sku` exists but does not have a price set for `token`', async function () {
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
const erc20Token = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.purchaseFor(recipient, erc20Token.address, sku, quantity, userData, {from: purchaser}),
'Sale: non-existent sku token'
);
});
it('should perform a purchase', async function () {
await doStart.bind(this)();
const quantity = One;
const receipt = await this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
});
expectEvent(receipt, 'Purchase', {
purchaser: purchaser,
recipient: recipient,
token: this.ethTokenAddress,
sku: sku,
quantity: One,
userData: userData,
totalPrice: this.tokenPrice.mul(One),
extData: bytes32ArraysToBytesPacked([[], [], []]),
});
});
});
describe('estimatePurchase()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
await doUpdateSkuPricing.bind(this)();
});
it('reverts if the sale has not started', async function () {
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Startable: not started'
);
});
it('reverts if the sale is paused', async function () {
await doStart.bind(this)();
await this.contract.pause({from: owner});
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Pausable: paused'
);
});
it('reverts if `recipient` is the zero address', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(ZeroAddress, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Sale: zero address recipient'
);
});
it('reverts if `token` is the zero address', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(recipient, ZeroAddress, sku, quantity, userData, {from: purchaser}),
'Sale: zero address token'
);
});
it('reverts if `quantity` is zero', async function () {
await doStart.bind(this)();
const quantity = Zero;
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Sale: zero quantity purchase'
);
});
it('reverts if `sku` does not exist', async function () {
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, stringToBytes32('otherSku'), quantity, userData, {from: purchaser}),
'Sale: non-existent sku'
);
});
it('reverts if `quantity` is greater than the maximum purchase quantity', async function () {
await doStart.bind(this)();
const quantity = skuMaxQuantityPerPurchase.addn(1);
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Sale: above max quantity'
);
});
it('reverts if `quantity` is greater than the remaining supply', async function () {
await doStart.bind(this)();
const quantity = skuMaxQuantityPerPurchase;
await this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
});
await expectRevert(
this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser}),
'Sale: insufficient supply'
);
});
it('reverts if `sku` exists but does not have a price set for `token`', async function () {
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
const erc20Token = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
await doStart.bind(this)();
const quantity = One;
await expectRevert(
this.contract.estimatePurchase(recipient, erc20Token.address, sku, quantity, userData, {from: purchaser}),
'Sale: non-existent sku token'
);
});
it('should estimate the computed final total amount to pay', async function () {
await doStart.bind(this)();
const quantity = One;
const priceInfo = await this.contract.estimatePurchase(recipient, this.ethTokenAddress, sku, quantity, userData, {from: purchaser});
priceInfo.totalPrice.should.be.bignumber.equal(ethPrice.mul(quantity));
priceInfo.pricingData.length.should.equal(0);
});
});
describe('getSkuInfo()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
await doUpdateSkuPricing.bind(this)();
});
it('reverts if `sku` does not exist', async function () {
await expectRevert(this.contract.getSkuInfo(stringToBytes32('otherSku')), 'Sale: non-existent sku');
});
it('should return the information relative to a SKU', async function () {
const skuInfo = await this.contract.getSkuInfo(sku);
skuInfo.totalSupply.should.be.bignumber.equal(skuTotalSupply);
skuInfo.remainingSupply.should.be.bignumber.equal(skuTotalSupply);
skuInfo.maxQuantityPerPurchase.should.be.bignumber.equal(skuMaxQuantityPerPurchase);
skuInfo.notificationsReceiver.should.be.equal(skuNotificationsReceiver);
skuInfo.tokens.length.should.be.equal(1);
skuInfo.tokens[0].should.be.equal(this.ethTokenAddress);
skuInfo.prices.length.should.be.equal(1);
skuInfo.prices[0].should.be.bignumber.equal(ethPrice);
});
});
describe('getSkus()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
});
it('should return the list of created SKU identifiers', async function () {
const skus = await this.contract.getSkus();
skus.length.should.be.equal(1);
skus[0].should.be.equal(sku);
});
});
describe('_createSku()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
});
it('reverts if `totalSupply` is zero', async function () {
await expectRevert(doCreateSku.bind(this)({skuTotalSupply: Zero}), 'Sale: zero supply');
});
it('reverts if `sku` already exists', async function () {
await doCreateSku.bind(this)();
await expectRevert(doCreateSku.bind(this)(), 'Sale: sku already created');
});
it('reverts if `notificationsReceiver` is not the zero address and is not a contract address', async function () {
await expectRevert(doCreateSku.bind(this)({skuNotificationsReceiver: purchaser}), 'Sale: non-contract receiver');
});
it('reverts if the update results in too many SKUs', async function () {
await doCreateSku.bind(this)();
await doCreateSku.bind(this)({sku: stringToBytes32('otherSku')});
await expectRevert(doCreateSku.bind(this)({sku: stringToBytes32('anotherSku')}), 'Sale: too many skus');
});
it('should create a SKU', async function () {
const skusBefore = await this.contract.getSkus();
skusBefore.length.should.be.equal(0);
await doCreateSku.bind(this)();
const skusAfter = await this.contract.getSkus();
skusAfter.length.should.be.equal(1);
(skusAfter[0] === sku).should.be.true;
const skuInfo = await this.contract.getSkuInfo(sku);
skuInfo.totalSupply.should.be.bignumber.equal(skuTotalSupply);
skuInfo.remainingSupply.should.be.bignumber.equal(skuTotalSupply);
skuInfo.maxQuantityPerPurchase.should.be.bignumber.equal(skuMaxQuantityPerPurchase);
skuInfo.notificationsReceiver.should.be.equal(ZeroAddress);
skuInfo.tokens.length.should.equal(0);
skuInfo.prices.length.should.equal(0);
});
it('should emit the SkuCreation event', async function () {
const receipt = await doCreateSku.bind(this)();
expectEvent(receipt, 'SkuCreation', {
sku: sku,
totalSupply: skuTotalSupply,
maxQuantityPerPurchase: skuMaxQuantityPerPurchase,
notificationsReceiver: skuNotificationsReceiver,
});
});
});
describe('_setTokenPrices()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
});
it('reverts if one of the `tokens` is the zero address', async function () {
await expectRevert(this.contract.setTokenPrices(sku, [ZeroAddress], [ethPrice]), 'Sale: zero address token');
});
it('reverts if the sku token price set exceeds the defined tokens per-sku capacity', async function () {
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
const erc20Token1 = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
const erc20Token2 = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
const erc20Token3 = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
await expectRevert(
this.contract.setTokenPrices(sku, [erc20Token1.address, erc20Token2.address, erc20Token3.address], [ethPrice, ethPrice, ethPrice]),
'Sale: too many tokens'
);
});
it('should disable a SKU token if provided a zero price', async function () {
await doUpdateSkuPricing.bind(this)();
await doUpdateSkuPricing.bind(this)({useErc20: true});
// await this.contract.getSkuInfo(sku);
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.tokens.length.should.equal(2);
skuInfoBefore.prices.length.should.equal(2);
await this.contract.setTokenPrices(sku, [this.ethTokenAddress], [Zero]);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.tokens.length.should.equal(1);
skuInfoAfter.prices.length.should.equal(1);
skuInfoAfter.tokens[0].should.equal(this.erc20Token.address);
skuInfoAfter.prices[0].should.be.bignumber.equal(erc20Price);
});
it('should add support for a SKU token if provided a price for an unsupported token', async function () {
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.tokens.length.should.equal(0);
skuInfoBefore.prices.length.should.equal(0);
await this.contract.setTokenPrices(sku, [this.ethTokenAddress], [ethPrice]);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.tokens.length.should.equal(1);
skuInfoAfter.prices.length.should.equal(1);
skuInfoAfter.tokens[0].should.equal(this.ethTokenAddress);
skuInfoAfter.prices[0].should.be.bignumber.equal(ethPrice);
});
it('should update a SKU token price', async function () {
await doUpdateSkuPricing.bind(this)();
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.prices.length.should.equal(1);
skuInfoBefore.prices[0].should.be.bignumber.equal(ethPrice);
const updatedPrice = ethPrice.muln(2);
await this.contract.setTokenPrices(sku, [this.ethTokenAddress], [updatedPrice]);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.prices.length.should.equal(1);
skuInfoAfter.prices[0].should.be.bignumber.equal(updatedPrice);
});
});
describe('_validation()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
await doCreateSku.bind(this)();
await doUpdateSkuPricing.bind(this)();
});
it('reverts if `purchase.recipient` is the zero address', async function () {
await expectRevert(
this.contract.validation(ZeroAddress, this.ethTokenAddress, sku, One, userData, Zero, [], [], []),
'Sale: zero address recipient'
);
});
it('reverts if `purchase.token` is the zero address', async function () {
await expectRevert(this.contract.validation(recipient, ZeroAddress, sku, One, userData, Zero, [], [], []), 'Sale: zero address token');
});
it('reverts if `purchase.quantity` is zero', async function () {
await expectRevert(
this.contract.validation(recipient, this.ethTokenAddress, sku, Zero, userData, Zero, [], [], []),
'Sale: zero quantity purchase'
);
});
it('reverts if `purchase.sku` does not exist', async function () {
await expectRevert(
this.contract.validation(recipient, this.ethTokenAddress, stringToBytes32('otherSku'), One, userData, Zero, [], [], []),
'Sale: non-existent sku'
);
});
it('reverts if `purchase.quantity` is greater than the maximum purchase quantity', async function () {
await expectRevert(
this.contract.validation(recipient, this.ethTokenAddress, sku, skuMaxQuantityPerPurchase.addn(1), userData, Zero, [], [], []),
'Sale: above max quantity'
);
});
it('reverts if `purchase.quantity` is greater than the remaining supply', async function () {
await doStart.bind(this)();
const quantity = skuMaxQuantityPerPurchase;
await this.contract.purchaseFor(recipient, this.ethTokenAddress, sku, quantity, userData, {
from: purchaser,
value: ethPrice.mul(quantity),
});
await expectRevert(
this.contract.validation(recipient, this.ethTokenAddress, sku, quantity, userData, Zero, [], [], []),
'Sale: insufficient supply'
);
});
it('reverts if `purchase.sku` exists but does not have a price set for `purchase.token`', async function () {
const registry = await artifacts.require('ForwarderRegistry').new();
const forwarder = await artifacts.require('UniversalForwarder').new();
const erc20Token = await ERC20.new([owner], [erc20TotalSupply], registry.address, forwarder.address, {from: owner});
await expectRevert(
this.contract.validation(recipient, erc20Token.address, sku, One, userData, Zero, [], [], []),
'Sale: non-existent sku token'
);
});
});
describe('_delivery()', function () {
beforeEach(async function () {
await doDeploy.bind(this)();
});
it('should not update the SKU total supply if the supply is unlimited', async function () {
const skuTotalSupply = await this.contract.SUPPLY_UNLIMITED();
await doCreateSku.bind(this)({skuTotalSupply: skuTotalSupply});
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.remainingSupply.should.be.bignumber.equal(skuTotalSupply);
await this.contract.delivery(ZeroAddress, ZeroAddress, sku, One, userData, Zero, [], [], []);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.remainingSupply.should.be.bignumber.equal(skuTotalSupply);
});
it('should update the SKU total supply if the supply is limited', async function () {
await doCreateSku.bind(this)();
const skuInfoBefore = await this.contract.getSkuInfo(sku);
skuInfoBefore.remainingSupply.should.be.bignumber.equal(skuTotalSupply);
await this.contract.delivery(ZeroAddress, ZeroAddress, sku, One, userData, Zero, [], [], []);
const skuInfoAfter = await this.contract.getSkuInfo(sku);
skuInfoAfter.remainingSupply.should.be.bignumber.equal(skuTotalSupply.subn(1));
});
});
describe('_notification()', function () {
beforeEach(async function () {
this.purchaseNotificationsReceiver = await PurchaseNotificationsReceiver.new();
await doDeploy.bind(this)();
await doCreateSku.bind(this)({skuNotificationsReceiver: this.purchaseNotificationsReceiver.address});
await doUpdateSkuPricing.bind(this)();
});
it('should emit the Purchase event', async function () {
const pricingData = [stringToBytes32('abcde')];
const paymentData = [];
const deliveryData = [uintToBytes32(Two), addressToBytes32(this.ethTokenAddress)];
const receipt = await this.contract.notification(
recipient,
this.ethTokenAddress,
sku,
One,
userData,
ethPrice,
pricingData,
paymentData,
deliveryData,
{from: purchaser}
);
expectEvent(receipt, 'Purchase', {
purchaser: purchaser,
recipient: recipient,
token: this.ethTokenAddress,
sku: sku,
quantity: One,
userData: userData,
totalPrice: ethPrice,
extData: bytes32ArraysToBytesPacked([pricingData, paymentData, deliveryData]),
});
});
it("should call the SKU's notification receiver if defined", async function () {
const receipt = await this.contract.notification(recipient, this.ethTokenAddress, sku, One, userData, ethPrice, [], [], [], {from: purchaser});
await expectEvent.inTransaction(receipt.tx, this.purchaseNotificationsReceiver, 'PurchaseNotificationReceived', {});
});
});
});